From e5099bfdfeaf0a224434eda075afdd7d4c896803 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang <59727193+horriblename@users.noreply.github.com> Date: Sat, 23 Jul 2022 11:35:21 +0200 Subject: [PATCH 01/24] Sixel patch (#1) * new struct type sixel * add sixel detection logic in printReg * new ShowSixels that prints sixels to the screen * placeholder function UnshowSixels * run `ui.ShowSixels` after `ui.screen.Sync` * fix sixel placed on wrong coordinates * change sixel termination sequence to prevent other escape sequence being run inside it * process multi-line sixel sequence * fixed bug where string before sixel image is not printed * add sixelDimPx and pxToCells * add getTermPixels for unix * remove unused method UnshowSixels * fix: remove unneeded ShowSixels * add new fields sixel.wPx,hPx and reg.sixels * modify sixel processing logic detecting sixels in preview script now fills corresponding area with braille blank `\u2800` and saves sixel to reg.sixels * modify sixel processing logic printReg doesn't process sixels directly anymore * reset sixels buffer at the start of `draw` * rename `ui.ShowSixels` to `showSixels` * add constants `gSixelBegin` and `gSixelTerminate` * add check to prevent arbitrary escape code being passed to stdin * fix cursor out of place in command line mode * fix bug where sixel in drawn at old location after horizontal resize * add ui.wPx and ui.hPx * buffer sixel sequences before printing * add check for valid terminal size(px) before previewing sixel * clean up * placeholder function getTermPixels for windows * function sixelDimPx now considers image size given in the optional raster attributes field * change sixel image alignment to emulate behavior of a terminal - images used to always draw on a new line - images is now placed where it starts: ``` printf 'abc xyz' ``` would result in: ``` abc +------+ | img | | here | +------+ xyz ``` old behavior: ``` abc +------+ | img | | here | +------+ xyz ``` * fix bug where raster attributes are wrongly parsed in sixelDimPx * prevent drawing sixels while menu is active * introduce sixelScreen struct and refactor screen width,height in px to use new struct * fix prevent nested sixel sequences * fix bug where rejected sixels cause indexing error * add "alternating filler" to trick tcell into redrawing when switching between different images * fix filler string wrongly indented * add comment * replace pxToCells() with sixelScreen.pxToCells() * add trim sixel height during preview * add tests for trimSixelHeight * prevent sixel redrawing during input prompts * change sixel filler to braille space * clean up * use strings.Index instead of regex for simple search --- eval.go | 1 + misc.go | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++ misc_test.go | 45 ++++++++++++++++ nav.go | 88 +++++++++++++++++++++++++++++-- os.go | 9 ++++ os_windows.go | 4 ++ ui.go | 83 ++++++++++++++++++++++++++++- 7 files changed, 365 insertions(+), 7 deletions(-) diff --git a/eval.go b/eval.go index d97173be..e2dd0ab2 100644 --- a/eval.go +++ b/eval.go @@ -1700,6 +1700,7 @@ func (e *callExpr) eval(app *app, args []string) { if !app.nav.init { return } + app.ui.sxScreen.updateSizes(app.ui.screen.Size()) app.ui.renew() app.ui.screen.Sync() if app.nav.height != app.ui.wins[0].h { diff --git a/misc.go b/misc.go index 451b7601..f78c0f85 100644 --- a/misc.go +++ b/misc.go @@ -295,6 +295,148 @@ func max(a, b int) int { return b } +func mod(a, b int) int { + return (a%b + b) % b +} + +var reNumber = regexp.MustCompile(`^[0-9]+`) + +// needs some testing +func sixelDimPx(s string) (w int, h int) { + // TODO maybe take into account pixel aspect ratio + + // General sixel sequence: + // DCS ;;; q [" ]; ST + // DCS is "ESC P" + // We are not interested in P1~P3 + // the optional raster attributes may contain the image size in pixels + // ST is the terminating string "ESC \" + i := strings.Index(s, "q") + 1 + if i == 0 { + // syntax error + return -1, -1 + } + + // Start of (optional) Raster Attributes + // " Pan ; Pad; Ph; Pv + // pixel aspect ratio = Pan/Pad + // We are only interested in Ph and Pv (horizontal and vertical size in px) + if s[i] == '"' { + i++ + b := strings.Index(s[i:], ";") + // pan := strconv.Atoi(s[a:b]) + i += b + 1 + b = strings.Index(s[i:], ";") + // pad := strconv.Atoi(s[a:b]) + + i += b + 1 + b = strings.Index(s[i:], ";") + ph, err1 := strconv.Atoi(s[i : i+b]) + + i += b + 1 + b = strings.Index(s[i:], "#") + pv, err2 := strconv.Atoi(s[i : i+b]) + i += b + + if err1 != nil || err2 != nil { + goto main_body // keep trying + } + + // TODO + // ph and pv are more like suggestions, it's still possible to go over the + // reported size, so we might need to parse the entire main body anyway + return ph, pv + } + +main_body: + var wi int + for ; i < len(s)-2; i++ { + c := s[i] + switch { + case '?' <= c && c <= '~': + wi++ + case c == '-': + w = max(w, wi) + wi = 0 + h++ + case c == '$': + w = max(w, wi) + wi = 0 + case c == '!': + m := reNumber.FindString(s[i+1:]) + if m == "" { + // syntax error + return -1, -1 + } + if s[i+1+len(m)] < '?' || s[i+1+len(m)] > '~' { + // syntax error + return -1, -1 + } + n, _ := strconv.Atoi(m) + wi += n - 1 + default: + } + } + if s[len(s)-3] != '-' { + w = max(w, wi) + h++ // add newline on last row + } + return w, h * 6 +} + +// maybe merge with sixelDimPx() +func trimSixelHeight(s string, maxh int) (string, int) { + var h int + maxh = maxh - (maxh % 6) + + i := strings.Index(s, "q") + 1 + if i == 0 { + // syntax error + return "", -1 + } + + if s[i] == '"' { + i++ + for j := 0; j < 3; j++ { + b := strings.Index(s[i:], ";") + i += b + 1 + } + b := strings.Index(s[i:], "#") + pv, err := strconv.Atoi(s[i : i+b]) + + if err == nil && pv > maxh { + mh := strconv.Itoa(maxh) + s = s[:i] + mh + s[i+b:] + i += len(mh) + } else { + i += b + } + } + + for h < maxh { + k := strings.IndexRune(s[i+1:], '-') + if k == -1 { + if s[len(s)-3] != '-' { + h += 6 + i = len(s) - 3 + } + break + } + i += k + 1 + h += 6 + } + + if i == 0 { + return s, 6 + } + + if len(s) > i+3 { + return s[:i+1] + "\x1b\\", h + } + + return s, h +} + // We don't need no generic code // We don't need no type control // No dark templates in compiler diff --git a/misc_test.go b/misc_test.go index 3568fa2f..062d34bc 100644 --- a/misc_test.go +++ b/misc_test.go @@ -243,3 +243,48 @@ func TestNaturalLess(t *testing.T) { } } } + +func TestTrimSixelHeight(t *testing.T) { + tests := []struct { + si string + hi int + so string + ho int + }{ + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 6, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 30, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 11, + "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 30, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 11, + "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, + }, + } + + for i, test := range tests { + if got1, got2 := trimSixelHeight(test.si, test.hi); got1 != test.so || got2 != test.ho { + t.Errorf("test #%d expected height %d, got %d and string %s", i, test.ho, got2, got1[1:]) + } + } + +} diff --git a/nav.go b/nav.go index 1f205207..9a6d4e75 100644 --- a/nav.go +++ b/nav.go @@ -18,6 +18,15 @@ import ( times "github.com/djherbis/times" ) +const ( + gSixelBegin = "\033P" + gSixelTerminate = "\033\\" +) + +var ( + gSixelFiller = '\u2800' +) + type linkState byte const ( @@ -661,7 +670,8 @@ func (nav *nav) previewLoop(ui *ui) { nav.volatilePreview = false } if len(path) != 0 { - nav.preview(path, win) + win := ui.wins[len(ui.wins)-1] + nav.preview(path, &ui.sxScreen, win) prev = path } } @@ -745,8 +755,7 @@ func (nav *nav) previewDir(dir *dir, win *win) { } -func (nav *nav) preview(path string, win *win) { - +func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { reg := ®{loadTime: time.Now(), path: path} defer func() { nav.regChan <- reg }() @@ -800,14 +809,83 @@ func (nav *nav) preview(path string, win *win) { buf := bufio.NewScanner(reader) - for i := 0; i < win.h && buf.Scan(); i++ { + var sixelBuffer []string + processingSixel := false + for i := 0; (processingSixel || len(reg.lines) < win.h) && buf.Scan(); i++ { + text := buf.Text() + for _, r := range buf.Text() { if r == 0 { reg.lines = []string{"\033[7mbinary\033[0m"} return } } - reg.lines = append(reg.lines, buf.Text()) + if sxScreen.wpx > 0 && sxScreen.hpx > 0 { + if a := strings.Index(text, gSixelBegin); !processingSixel && a >= 0 { + reg.lines = append(reg.lines, text[:a]) + text = text[a:] + processingSixel = true + } + + if processingSixel { + var lookFrom int + if text[:2] == gSixelBegin { + lookFrom = 2 + } + if b := strings.IndexByte(text[lookFrom:], gEscapeCode); b >= 0 { + b += lookFrom + if len(text) > b && text[b+1] == '\\' { + sixelBuffer = append(sixelBuffer, text[:b+2]) + sx := strings.Join(sixelBuffer, "") + + xoff := runeSliceWidth([]rune(reg.lines[len(reg.lines)-1])) + 2 + yoff := len(reg.lines) - 1 + maxh := (win.h - yoff) * sxScreen.fonth + w, h := sixelDimPx(sx) + if w < 0 || h < 0 { + goto discard_sixel + } + sx, h = trimSixelHeight(sx, maxh) + wc, hc := sxScreen.pxToCells(w, h) + + reg.sixels = append(reg.sixels, sixel{xoff, yoff, w, h, sx}) + fill := sxScreen.filler(path, wc) + paddedfill := strings.Repeat(" ", xoff-2) + fill + reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + fill + for j := 1; j < hc; j++ { + reg.lines = append(reg.lines, paddedfill) + } + + reg.lines = append(reg.lines, text[b+2:]) + processingSixel = false + continue + } else { // deal with unexpected control sequence + goto discard_sixel + } + } + sixelBuffer = append(sixelBuffer, text) + continue + + discard_sixel: + emptyLines := min(win.h-len(reg.lines), len(sixelBuffer)-1) + reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + sixelBuffer[0] + if emptyLines > 0 { + reg.lines = append(reg.lines, sixelBuffer[1:emptyLines+1]...) + } + reg.lines = append(reg.lines, text) + processingSixel = false + continue + } + } + reg.lines = append(reg.lines, text) + } + + if processingSixel && len(sixelBuffer) > 0 { + emptyLines := min(win.h-len(reg.lines), len(sixelBuffer)-1) + reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + sixelBuffer[0] + if emptyLines > 0 { + reg.lines = append(reg.lines, sixelBuffer[1:emptyLines+1]...) + } } if buf.Err() != nil { diff --git a/os.go b/os.go index bfbbb58f..a3a2c13e 100644 --- a/os.go +++ b/os.go @@ -241,3 +241,12 @@ func diskFree(wd string) string { // Available blocks * size per block = available space in bytes return " df: " + humanize(int64(uint64(stat.Bavail)*uint64(stat.Bsize))) } + +func getTermPixels() (w, h int, err error) { + fd := int(os.Stdin.Fd()) + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return -1, -1, err + } + return int(ws.Xpixel), int(ws.Ypixel), nil +} diff --git a/os_windows.go b/os_windows.go index faff5355..305165ae 100644 --- a/os_windows.go +++ b/os_windows.go @@ -200,3 +200,7 @@ func diskFree(wd string) string { } return " df: " + humanize(int64(free)) } + +func getTermPixels() (w, h int, err error) { + return -1, -1, nil +} diff --git a/ui.go b/ui.go index e6a71e84..d5e1929e 100644 --- a/ui.go +++ b/ui.go @@ -252,7 +252,7 @@ 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) { +func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool, sxs *sixelScreen) { if reg == nil { return } @@ -274,6 +274,13 @@ func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool) { st = win.print(screen, 2, i, st, l) } + + for _, sx := range reg.sixels { + s := sx + s.x += win.x + s.y += win.y + sxs.sx = append(sxs.sx, s) + } } var gThisYear = time.Now().Year() @@ -559,8 +566,22 @@ func getWins(screen tcell.Screen) []*win { return wins } +type sixel struct { + x, y, wPx, hPx int + str string +} + +type sixelScreen struct { + wpx, hpx int + fontw, fonth int + sx []sixel + lastFile string + altFill bool +} + type ui struct { screen tcell.Screen + sxScreen sixelScreen polling bool wins []*win promptWin *win @@ -605,6 +626,8 @@ func newUI(screen tcell.Screen) *ui { currentFile: "", } + ui.sxScreen.updateSizes(screen.Size()) + go ui.pollEvents() return ui @@ -686,6 +709,7 @@ type reg struct { loadTime time.Time path string lines []string + sixels []sixel } func (ui *ui) loadFile(app *app, volatile bool) { @@ -933,6 +957,7 @@ func (ui *ui) draw(nav *nav) { ui.screen.SetContent(i, j, ' ', nil, st) } } + ui.sxScreen.clear() ui.drawPromptLine(nav) @@ -987,7 +1012,7 @@ func (ui *ui) draw(nav *nav) { &dirStyle{colors: ui.styles, icons: ui.icons, role: Preview}, nav.previewLoading) } else if curr.Mode().IsRegular() { - preview.printReg(ui.screen, ui.regPrev, nav.previewLoading) + preview.printReg(ui.screen, ui.regPrev, nav.previewLoading, &ui.sxScreen) } } } @@ -1017,6 +1042,10 @@ func (ui *ui) draw(nav *nav) { } ui.screen.Show() + if ui.menuBuf == nil && ui.cmdPrefix == "" && len(ui.sxScreen.sx) > 0 { + ui.showSixels() + } + } func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok bool) { @@ -1397,3 +1426,53 @@ func listMatches(screen tcell.Screen, matches []string, selectedInd int) *bytes. return b } + +func (sxs *sixelScreen) clear() { + sxs.sx = nil +} + +// fillers are used to control when tcell redraws the region where a sixel image is drawn. +// alternating between bold("ESC [1m") and regular is to clear the image before drawing a new one. +func (sxs *sixelScreen) filler(path string, l int) (fill string) { + if path != sxs.lastFile { + sxs.altFill = !sxs.altFill + sxs.lastFile = path + } + + if sxs.altFill { + fill = "\033[1m" + defer func() { + fill += "\033[0m" + }() + } + + fill += strings.Repeat(string(gSixelFiller), l) + return +} + +func (sxs *sixelScreen) updateSizes(wc, hc int) { + var err error + sxs.wpx, sxs.hpx, err = getTermPixels() + if err != nil { + sxs.wpx, sxs.hpx = -1, -1 + log.Printf("getting terminal pixel size: %s", err) + } + + sxs.fontw = sxs.wpx / wc + sxs.fonth = sxs.hpx / hc +} + +func (xsx *sixelScreen) pxToCells(x, y int) (int, int) { + return x/xsx.fontw + 1, y/xsx.fonth + 1 +} + +func (ui *ui) showSixels() { + var buf strings.Builder + buf.WriteString("\0337") + for _, sixel := range ui.sxScreen.sx { + buf.WriteString(fmt.Sprintf("\033[%d;%dH", sixel.y+1, sixel.x+1)) + buf.WriteString(sixel.str) + } + buf.WriteString("\0338") + fmt.Print(buf.String()) +} From 86f5bc986b0abf06134e6cdcb61ecc35f2913ec5 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Tue, 13 Sep 2022 13:55:59 +0200 Subject: [PATCH 02/24] fix comment --- misc.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/misc.go b/misc.go index f78c0f85..ed0fc1b0 100644 --- a/misc.go +++ b/misc.go @@ -306,10 +306,11 @@ func sixelDimPx(s string) (w int, h int) { // TODO maybe take into account pixel aspect ratio // General sixel sequence: - // DCS ;;; q [" ]; ST + // DCS ;;; q [" ] ST // DCS is "ESC P" // We are not interested in P1~P3 - // the optional raster attributes may contain the image size in pixels + // the optional raster attributes may contain the 'reported' image size in pixels + // (The actual image can be larger, but is at least this big) // ST is the terminating string "ESC \" i := strings.Index(s, "q") + 1 if i == 0 { From 3e66b79574f29bb1e226e0fad68ec8d23cd8776c Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Tue, 13 Sep 2022 13:56:59 +0200 Subject: [PATCH 03/24] refactor: remove nested if-block --- nav.go | 68 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/nav.go b/nav.go index 9a6d4e75..d3ed06cf 100644 --- a/nav.go +++ b/nav.go @@ -821,50 +821,54 @@ func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { } } if sxScreen.wpx > 0 && sxScreen.hpx > 0 { + var lookFrom int if a := strings.Index(text, gSixelBegin); !processingSixel && a >= 0 { reg.lines = append(reg.lines, text[:a]) text = text[a:] processingSixel = true + lookFrom = 2 } if processingSixel { - var lookFrom int - if text[:2] == gSixelBegin { - lookFrom = 2 + b := strings.IndexByte(text[lookFrom:], gEscapeCode) + if b < 0 { + sixelBuffer = append(sixelBuffer, text) + continue } - if b := strings.IndexByte(text[lookFrom:], gEscapeCode); b >= 0 { - b += lookFrom - if len(text) > b && text[b+1] == '\\' { - sixelBuffer = append(sixelBuffer, text[:b+2]) - sx := strings.Join(sixelBuffer, "") - - xoff := runeSliceWidth([]rune(reg.lines[len(reg.lines)-1])) + 2 - yoff := len(reg.lines) - 1 - maxh := (win.h - yoff) * sxScreen.fonth - w, h := sixelDimPx(sx) - if w < 0 || h < 0 { - goto discard_sixel - } - sx, h = trimSixelHeight(sx, maxh) - wc, hc := sxScreen.pxToCells(w, h) - - reg.sixels = append(reg.sixels, sixel{xoff, yoff, w, h, sx}) - fill := sxScreen.filler(path, wc) - paddedfill := strings.Repeat(" ", xoff-2) + fill - reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + fill - for j := 1; j < hc; j++ { - reg.lines = append(reg.lines, paddedfill) - } - reg.lines = append(reg.lines, text[b+2:]) - processingSixel = false - continue - } else { // deal with unexpected control sequence + b += lookFrom + if len(text) > b && text[b+1] == '\\' { + sixelBuffer = append(sixelBuffer, text[:b+2]) + sx := strings.Join(sixelBuffer, "") + + xoff := runeSliceWidth([]rune(reg.lines[len(reg.lines)-1])) + 2 + yoff := len(reg.lines) - 1 + maxh := (win.h - yoff) * sxScreen.fonth + w, h := sixelDimPx(sx) + if w < 0 || h < 0 { + log.Printf("parsing preview of %s: invalid sixel sequence", path) //DEBUG goto discard_sixel } + sx, h = trimSixelHeight(sx, maxh) + wc, hc := sxScreen.pxToCells(w, h) + + reg.sixels = append(reg.sixels, sixel{xoff, yoff, w, h, sx}) + fill := sxScreen.filler(path, wc) + paddedfill := strings.Repeat(" ", xoff-2) + fill + reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + fill + for j := 1; j < hc; j++ { + reg.lines = append(reg.lines, paddedfill) + } + + reg.lines = append(reg.lines, text[b+2:]) + processingSixel = false + continue + } else { // deal with unexpected control sequence + log.Printf("parsing preview of %s: illegal escape sequence ESC %c", path, text[b+1]) // DEBUG + log.Printf("lookFrom %d", lookFrom) + log.Printf("full line: %s", text) + goto discard_sixel } - sixelBuffer = append(sixelBuffer, text) - continue discard_sixel: emptyLines := min(win.h-len(reg.lines), len(sixelBuffer)-1) From 3ab5a949e207df143b8e212fd8dfe33c500aab60 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 21 Oct 2022 13:48:35 +0200 Subject: [PATCH 04/24] refactor: move sixel related stuff into sixel.go --- misc.go | 139 ----------------------------------- nav.go | 9 --- sixel.go | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ui.go | 65 +--------------- 4 files changed, 221 insertions(+), 212 deletions(-) create mode 100644 sixel.go diff --git a/misc.go b/misc.go index ed0fc1b0..84b66cf1 100644 --- a/misc.go +++ b/misc.go @@ -299,145 +299,6 @@ func mod(a, b int) int { return (a%b + b) % b } -var reNumber = regexp.MustCompile(`^[0-9]+`) - -// needs some testing -func sixelDimPx(s string) (w int, h int) { - // TODO maybe take into account pixel aspect ratio - - // General sixel sequence: - // DCS ;;; q [" ] ST - // DCS is "ESC P" - // We are not interested in P1~P3 - // the optional raster attributes may contain the 'reported' image size in pixels - // (The actual image can be larger, but is at least this big) - // ST is the terminating string "ESC \" - i := strings.Index(s, "q") + 1 - if i == 0 { - // syntax error - return -1, -1 - } - - // Start of (optional) Raster Attributes - // " Pan ; Pad; Ph; Pv - // pixel aspect ratio = Pan/Pad - // We are only interested in Ph and Pv (horizontal and vertical size in px) - if s[i] == '"' { - i++ - b := strings.Index(s[i:], ";") - // pan := strconv.Atoi(s[a:b]) - i += b + 1 - b = strings.Index(s[i:], ";") - // pad := strconv.Atoi(s[a:b]) - - i += b + 1 - b = strings.Index(s[i:], ";") - ph, err1 := strconv.Atoi(s[i : i+b]) - - i += b + 1 - b = strings.Index(s[i:], "#") - pv, err2 := strconv.Atoi(s[i : i+b]) - i += b - - if err1 != nil || err2 != nil { - goto main_body // keep trying - } - - // TODO - // ph and pv are more like suggestions, it's still possible to go over the - // reported size, so we might need to parse the entire main body anyway - return ph, pv - } - -main_body: - var wi int - for ; i < len(s)-2; i++ { - c := s[i] - switch { - case '?' <= c && c <= '~': - wi++ - case c == '-': - w = max(w, wi) - wi = 0 - h++ - case c == '$': - w = max(w, wi) - wi = 0 - case c == '!': - m := reNumber.FindString(s[i+1:]) - if m == "" { - // syntax error - return -1, -1 - } - if s[i+1+len(m)] < '?' || s[i+1+len(m)] > '~' { - // syntax error - return -1, -1 - } - n, _ := strconv.Atoi(m) - wi += n - 1 - default: - } - } - if s[len(s)-3] != '-' { - w = max(w, wi) - h++ // add newline on last row - } - return w, h * 6 -} - -// maybe merge with sixelDimPx() -func trimSixelHeight(s string, maxh int) (string, int) { - var h int - maxh = maxh - (maxh % 6) - - i := strings.Index(s, "q") + 1 - if i == 0 { - // syntax error - return "", -1 - } - - if s[i] == '"' { - i++ - for j := 0; j < 3; j++ { - b := strings.Index(s[i:], ";") - i += b + 1 - } - b := strings.Index(s[i:], "#") - pv, err := strconv.Atoi(s[i : i+b]) - - if err == nil && pv > maxh { - mh := strconv.Itoa(maxh) - s = s[:i] + mh + s[i+b:] - i += len(mh) - } else { - i += b - } - } - - for h < maxh { - k := strings.IndexRune(s[i+1:], '-') - if k == -1 { - if s[len(s)-3] != '-' { - h += 6 - i = len(s) - 3 - } - break - } - i += k + 1 - h += 6 - } - - if i == 0 { - return s, 6 - } - - if len(s) > i+3 { - return s[:i+1] + "\x1b\\", h - } - - return s, h -} - // We don't need no generic code // We don't need no type control // No dark templates in compiler diff --git a/nav.go b/nav.go index d3ed06cf..8b50e05b 100644 --- a/nav.go +++ b/nav.go @@ -18,15 +18,6 @@ import ( times "github.com/djherbis/times" ) -const ( - gSixelBegin = "\033P" - gSixelTerminate = "\033\\" -) - -var ( - gSixelFiller = '\u2800' -) - type linkState byte const ( diff --git a/sixel.go b/sixel.go new file mode 100644 index 00000000..e18b6937 --- /dev/null +++ b/sixel.go @@ -0,0 +1,220 @@ +package main + +import ( + "fmt" + "log" + "regexp" + "strconv" + "strings" +) + +const ( + gSixelBegin = "\033P" + gSixelTerminate = "\033\\" +) + +var ( + gSixelFiller = '\u2800' +) + +type sixel struct { + x, y, wPx, hPx int + str string +} + +type sixelScreen struct { + wpx, hpx int + fontw, fonth int + sx []sixel + lastFile string + altFill bool +} + +func (sxs *sixelScreen) clear() { + sxs.sx = nil +} + +// fillers are used to control when tcell redraws the region where a sixel image is drawn. +// alternating between bold("ESC [1m") and regular is to clear the image before drawing a new one. +func (sxs *sixelScreen) filler(path string, l int) (fill string) { + if path != sxs.lastFile { + sxs.altFill = !sxs.altFill + sxs.lastFile = path + } + + if sxs.altFill { + fill = "\033[1m" + defer func() { + fill += "\033[0m" + }() + } + + fill += strings.Repeat(string(gSixelFiller), l) + return +} + +func (sxs *sixelScreen) updateSizes(wc, hc int) { + var err error + sxs.wpx, sxs.hpx, err = getTermPixels() + if err != nil { + sxs.wpx, sxs.hpx = -1, -1 + log.Printf("getting terminal pixel size: %s", err) + } + + sxs.fontw = sxs.wpx / wc + sxs.fonth = sxs.hpx / hc +} + +func (sxs *sixelScreen) pxToCells(x, y int) (int, int) { + return x/sxs.fontw + 1, y/sxs.fonth + 1 +} + +func (sxs *sixelScreen) showSixels() { + var buf strings.Builder + buf.WriteString("\0337") + for _, sixel := range sxs.sx { + buf.WriteString(fmt.Sprintf("\033[%d;%dH", sixel.y+1, sixel.x+1)) + buf.WriteString(sixel.str) + } + buf.WriteString("\0338") + fmt.Print(buf.String()) +} + +var reNumber = regexp.MustCompile(`^[0-9]+`) + +// needs some testing +func sixelDimPx(s string) (w int, h int) { + // TODO maybe take into account pixel aspect ratio + + // General sixel sequence: + // DCS ;;; q [" ] ST + // DCS is "ESC P" + // We are not interested in P1~P3 + // the optional raster attributes may contain the 'reported' image size in pixels + // (The actual image can be larger, but is at least this big) + // ST is the terminating string "ESC \" + i := strings.Index(s, "q") + 1 + if i == 0 { + // syntax error + return -1, -1 + } + + // Start of (optional) Raster Attributes + // " Pan ; Pad; Ph; Pv + // pixel aspect ratio = Pan/Pad + // We are only interested in Ph and Pv (horizontal and vertical size in px) + if s[i] == '"' { + i++ + b := strings.Index(s[i:], ";") + // pan := strconv.Atoi(s[a:b]) + i += b + 1 + b = strings.Index(s[i:], ";") + // pad := strconv.Atoi(s[a:b]) + + i += b + 1 + b = strings.Index(s[i:], ";") + ph, err1 := strconv.Atoi(s[i : i+b]) + + i += b + 1 + b = strings.Index(s[i:], "#") + pv, err2 := strconv.Atoi(s[i : i+b]) + i += b + + if err1 != nil || err2 != nil { + goto main_body // keep trying + } + + // TODO + // ph and pv are more like suggestions, it's still possible to go over the + // reported size, so we might need to parse the entire main body anyway + return ph, pv + } + +main_body: + var wi int + for ; i < len(s)-2; i++ { + c := s[i] + switch { + case '?' <= c && c <= '~': + wi++ + case c == '-': + w = max(w, wi) + wi = 0 + h++ + case c == '$': + w = max(w, wi) + wi = 0 + case c == '!': + m := reNumber.FindString(s[i+1:]) + if m == "" { + // syntax error + return -1, -1 + } + if s[i+1+len(m)] < '?' || s[i+1+len(m)] > '~' { + // syntax error + return -1, -1 + } + n, _ := strconv.Atoi(m) + wi += n - 1 + default: + } + } + if s[len(s)-3] != '-' { + w = max(w, wi) + h++ // add newline on last row + } + return w, h * 6 +} + +// maybe merge with sixelDimPx() +func trimSixelHeight(s string, maxh int) (string, int) { + var h int + maxh = maxh - (maxh % 6) + + i := strings.Index(s, "q") + 1 + if i == 0 { + // syntax error + return "", -1 + } + + if s[i] == '"' { + i++ + for j := 0; j < 3; j++ { + b := strings.Index(s[i:], ";") + i += b + 1 + } + b := strings.Index(s[i:], "#") + pv, err := strconv.Atoi(s[i : i+b]) + + if err == nil && pv > maxh { + mh := strconv.Itoa(maxh) + s = s[:i] + mh + s[i+b:] + i += len(mh) + } else { + i += b + } + } + + for h < maxh { + k := strings.IndexRune(s[i+1:], '-') + if k == -1 { + if s[len(s)-3] != '-' { + h += 6 + i = len(s) - 3 + } + break + } + i += k + 1 + h += 6 + } + + if i == 0 { + return s, 6 + } + + if len(s) > i+3 { + return s[:i+1] + "\x1b\\", h + } + + return s, h +} diff --git a/ui.go b/ui.go index d5e1929e..f8d9f3ec 100644 --- a/ui.go +++ b/ui.go @@ -566,19 +566,6 @@ func getWins(screen tcell.Screen) []*win { return wins } -type sixel struct { - x, y, wPx, hPx int - str string -} - -type sixelScreen struct { - wpx, hpx int - fontw, fonth int - sx []sixel - lastFile string - altFill bool -} - type ui struct { screen tcell.Screen sxScreen sixelScreen @@ -1043,7 +1030,7 @@ func (ui *ui) draw(nav *nav) { ui.screen.Show() if ui.menuBuf == nil && ui.cmdPrefix == "" && len(ui.sxScreen.sx) > 0 { - ui.showSixels() + ui.sxScreen.showSixels() } } @@ -1426,53 +1413,3 @@ func listMatches(screen tcell.Screen, matches []string, selectedInd int) *bytes. return b } - -func (sxs *sixelScreen) clear() { - sxs.sx = nil -} - -// fillers are used to control when tcell redraws the region where a sixel image is drawn. -// alternating between bold("ESC [1m") and regular is to clear the image before drawing a new one. -func (sxs *sixelScreen) filler(path string, l int) (fill string) { - if path != sxs.lastFile { - sxs.altFill = !sxs.altFill - sxs.lastFile = path - } - - if sxs.altFill { - fill = "\033[1m" - defer func() { - fill += "\033[0m" - }() - } - - fill += strings.Repeat(string(gSixelFiller), l) - return -} - -func (sxs *sixelScreen) updateSizes(wc, hc int) { - var err error - sxs.wpx, sxs.hpx, err = getTermPixels() - if err != nil { - sxs.wpx, sxs.hpx = -1, -1 - log.Printf("getting terminal pixel size: %s", err) - } - - sxs.fontw = sxs.wpx / wc - sxs.fonth = sxs.hpx / hc -} - -func (xsx *sixelScreen) pxToCells(x, y int) (int, int) { - return x/xsx.fontw + 1, y/xsx.fonth + 1 -} - -func (ui *ui) showSixels() { - var buf strings.Builder - buf.WriteString("\0337") - for _, sixel := range ui.sxScreen.sx { - buf.WriteString(fmt.Sprintf("\033[%d;%dH", sixel.y+1, sixel.x+1)) - buf.WriteString(sixel.str) - } - buf.WriteString("\0338") - fmt.Print(buf.String()) -} From 82bc5bb0958f527ffafda23494dbbb6feb18c92a Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Mon, 10 Apr 2023 13:19:40 +0200 Subject: [PATCH 05/24] remove unused function --- misc.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/misc.go b/misc.go index 84b66cf1..451b7601 100644 --- a/misc.go +++ b/misc.go @@ -295,10 +295,6 @@ func max(a, b int) int { return b } -func mod(a, b int) int { - return (a%b + b) % b -} - // We don't need no generic code // We don't need no type control // No dark templates in compiler From 3947975e589b12ffe80c623ad434c5991f027851 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Mon, 10 Apr 2023 15:18:28 +0200 Subject: [PATCH 06/24] allow long lines from previewer --- nav.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nav.go b/nav.go index 8b50e05b..1b57a1f9 100644 --- a/nav.go +++ b/nav.go @@ -20,6 +20,10 @@ import ( type linkState byte +const ( + previewerMaxLineSize int = 1048576 // 1 MB +) + const ( notLink linkState = iota working @@ -799,6 +803,8 @@ func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { } buf := bufio.NewScanner(reader) + buffer := make([]byte, 0) + buf.Buffer(buffer, previewerMaxLineSize) var sixelBuffer []string processingSixel := false From f8114fc632fb8216d5dd6000004e8ff448456dbb Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Mon, 10 Apr 2023 18:52:35 +0200 Subject: [PATCH 07/24] rework preview to accept only single-line sixels - multi-line support removed for simplicity in the code base --- nav.go | 82 +++++--------------------------------------------------- sixel.go | 38 +++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 77 deletions(-) diff --git a/nav.go b/nav.go index 1b57a1f9..244479de 100644 --- a/nav.go +++ b/nav.go @@ -803,90 +803,20 @@ func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { } buf := bufio.NewScanner(reader) - buffer := make([]byte, 0) - buf.Buffer(buffer, previewerMaxLineSize) + buf.Buffer(make([]byte, 0), previewerMaxLineSize) - var sixelBuffer []string - processingSixel := false - for i := 0; (processingSixel || len(reg.lines) < win.h) && buf.Scan(); i++ { + for i := 0; len(reg.lines) < win.h && buf.Scan(); i++ { text := buf.Text() - - for _, r := range buf.Text() { + for _, r := range text { if r == 0 { reg.lines = []string{"\033[7mbinary\033[0m"} return } } - if sxScreen.wpx > 0 && sxScreen.hpx > 0 { - var lookFrom int - if a := strings.Index(text, gSixelBegin); !processingSixel && a >= 0 { - reg.lines = append(reg.lines, text[:a]) - text = text[a:] - processingSixel = true - lookFrom = 2 - } - - if processingSixel { - b := strings.IndexByte(text[lookFrom:], gEscapeCode) - if b < 0 { - sixelBuffer = append(sixelBuffer, text) - continue - } - b += lookFrom - if len(text) > b && text[b+1] == '\\' { - sixelBuffer = append(sixelBuffer, text[:b+2]) - sx := strings.Join(sixelBuffer, "") - - xoff := runeSliceWidth([]rune(reg.lines[len(reg.lines)-1])) + 2 - yoff := len(reg.lines) - 1 - maxh := (win.h - yoff) * sxScreen.fonth - w, h := sixelDimPx(sx) - if w < 0 || h < 0 { - log.Printf("parsing preview of %s: invalid sixel sequence", path) //DEBUG - goto discard_sixel - } - sx, h = trimSixelHeight(sx, maxh) - wc, hc := sxScreen.pxToCells(w, h) - - reg.sixels = append(reg.sixels, sixel{xoff, yoff, w, h, sx}) - fill := sxScreen.filler(path, wc) - paddedfill := strings.Repeat(" ", xoff-2) + fill - reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + fill - for j := 1; j < hc; j++ { - reg.lines = append(reg.lines, paddedfill) - } - - reg.lines = append(reg.lines, text[b+2:]) - processingSixel = false - continue - } else { // deal with unexpected control sequence - log.Printf("parsing preview of %s: illegal escape sequence ESC %c", path, text[b+1]) // DEBUG - log.Printf("lookFrom %d", lookFrom) - log.Printf("full line: %s", text) - goto discard_sixel - } - - discard_sixel: - emptyLines := min(win.h-len(reg.lines), len(sixelBuffer)-1) - reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + sixelBuffer[0] - if emptyLines > 0 { - reg.lines = append(reg.lines, sixelBuffer[1:emptyLines+1]...) - } - reg.lines = append(reg.lines, text) - processingSixel = false - continue - } - } - reg.lines = append(reg.lines, text) - } - - if processingSixel && len(sixelBuffer) > 0 { - emptyLines := min(win.h-len(reg.lines), len(sixelBuffer)-1) - reg.lines[len(reg.lines)-1] = reg.lines[len(reg.lines)-1] + sixelBuffer[0] - if emptyLines > 0 { - reg.lines = append(reg.lines, sixelBuffer[1:emptyLines+1]...) - } + lines, sixels := renderPreviewLine(text, len(reg.lines), path, win, sxScreen) + reg.lines = append(reg.lines, lines...) + reg.sixels = append(reg.sixels, sixels...) } if buf.Err() != nil { diff --git a/sixel.go b/sixel.go index e18b6937..8366f9c2 100644 --- a/sixel.go +++ b/sixel.go @@ -50,7 +50,7 @@ func (sxs *sixelScreen) filler(path string, l int) (fill string) { } fill += strings.Repeat(string(gSixelFiller), l) - return + return fill } func (sxs *sixelScreen) updateSizes(wc, hc int) { @@ -82,6 +82,42 @@ func (sxs *sixelScreen) showSixels() { var reNumber = regexp.MustCompile(`^[0-9]+`) +func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen *sixelScreen) (lines []string, sixels []sixel) { + if sxScreen.wpx > 0 && sxScreen.hpx > 0 { + if a := strings.Index(text, gSixelBegin); a >= 0 { + if b := strings.Index(text[a:], gSixelTerminate); b >= 0 { + textbefore := text[:a] + s := text[a : a+b+len(gSixelTerminate)] + textafter := text[a+b+len(gSixelTerminate):] + wpx, hpx := sixelDimPx(s) + + if wpx >= 0 || hpx >= 0 { + xc := runeSliceWidth([]rune(textbefore)) + 2 + yc := linenr + maxh := (win.h - yc) * sxScreen.fonth + s, hpx = trimSixelHeight(s, maxh) + wc, hc := sxScreen.pxToCells(wpx, hpx) + fill := sxScreen.filler(fpath, wc) + + lines = append(lines, textbefore+fill) + + sixels = append(sixels, sixel{xc, yc, wpx, hpx, s}) + paddedfill := strings.Repeat(" ", xc-2) + fill + for j := 1; j < hc; j++ { + lines = append(lines, paddedfill) + } + + linesAfter, sixelsAfter := renderPreviewLine(textafter, linenr, fpath, win, sxScreen) + lines = append(lines, linesAfter...) + sixels = append(sixels, sixelsAfter...) + return lines, sixels + } + } + } + } + return []string{text}, sixels +} + // needs some testing func sixelDimPx(s string) (w int, h int) { // TODO maybe take into account pixel aspect ratio From cfef1d920a232c6c49030f47f876bb9c8c05fbe9 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Mon, 10 Apr 2023 18:53:23 +0200 Subject: [PATCH 08/24] add new option `sixel` --- opts.go | 2 ++ sixel.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/opts.go b/opts.go index c4a17681..e47852eb 100644 --- a/opts.go +++ b/opts.go @@ -47,6 +47,7 @@ var gOpts struct { mouse bool number bool preview bool + sixel bool relativenumber bool smartcase bool smartdia bool @@ -106,6 +107,7 @@ func init() { gOpts.mouse = false gOpts.number = false gOpts.preview = true + gOpts.sixel = false gOpts.relativenumber = false gOpts.smartcase = true gOpts.smartdia = false diff --git a/sixel.go b/sixel.go index 8366f9c2..81fb5b49 100644 --- a/sixel.go +++ b/sixel.go @@ -83,7 +83,7 @@ func (sxs *sixelScreen) showSixels() { var reNumber = regexp.MustCompile(`^[0-9]+`) func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen *sixelScreen) (lines []string, sixels []sixel) { - if sxScreen.wpx > 0 && sxScreen.hpx > 0 { + if gOpts.sixel && sxScreen.wpx > 0 && sxScreen.hpx > 0 { if a := strings.Index(text, gSixelBegin); a >= 0 { if b := strings.Index(text[a:], gSixelTerminate); b >= 0 { textbefore := text[:a] From 9368e2edf05541d616221533f082462b1ea81d16 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Thu, 13 Apr 2023 12:26:35 +0200 Subject: [PATCH 09/24] add eval logic and completion of `sixel` option --- complete.go | 3 +++ eval.go | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/complete.go b/complete.go index 8900518c..f5e03110 100644 --- a/complete.go +++ b/complete.go @@ -212,6 +212,9 @@ var ( "infotimefmtnew", "infotimefmtold", "truncatechar", + "sixel", + "nosixel", + "sixel!", } ) diff --git a/eval.go b/eval.go index e2dd0ab2..fa1db036 100644 --- a/eval.go +++ b/eval.go @@ -848,6 +848,27 @@ func (e *setExpr) eval(app *app, args []string) { return } gOpts.wrapscroll = !gOpts.wrapscroll + case "sixel": + if e.val == "" || e.val == "true" { + gOpts.sixel = true + } else if e.val == "false" { + gOpts.sixel = false + } else { + app.ui.echoerr("sixel: value should be empty, 'true', or 'false'") + return + } + case "nosixel": + if e.val != "" { + app.ui.echoerrf("nosixel: unexpected value: %s", e.val) + return + } + gOpts.sixel = false + case "sixel!": + if e.val != "" { + app.ui.echoerrf("sixel!: unexpected value: %s", e.val) + return + } + gOpts.wrapscroll = !gOpts.wrapscroll default: // any key with the prefix user_ is accepted as a user defined option if strings.HasPrefix(e.opt, "user_") { From b01b6e5b380cb8b9254f202eac3417d07d28aaec Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Thu, 13 Apr 2023 12:26:58 +0200 Subject: [PATCH 10/24] doc: new option 'sixel' --- doc.go | 5 +++++ docstring.go | 5 +++++ lf.1 | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/doc.go b/doc.go index 36ca4e48..eb7ad8f2 100644 --- a/doc.go +++ b/doc.go @@ -158,6 +158,7 @@ The following options can be used to customize the behavior of lf: shell string (default 'sh' for Unix and 'cmd' for Windows) shellflag string (default '-c' for Unix and '/c' for Windows) shellopts []string (default '') + sixel []string (default false) smartcase bool (default true) smartdia bool (default false) sortby string (default 'natural') @@ -848,6 +849,10 @@ Command line flag used to pass shell commands. List of shell options to pass to the shell executable. + sixel bool (default false) + +Render sixel images in preview + smartcase bool (default true) Override 'ignorecase' option when the pattern contains an uppercase character. diff --git a/docstring.go b/docstring.go index 57c5d4f4..dac2eb29 100644 --- a/docstring.go +++ b/docstring.go @@ -161,6 +161,7 @@ The following options can be used to customize the behavior of lf: shell string (default 'sh' for Unix and 'cmd' for Windows) shellflag string (default '-c' for Unix and '/c' for Windows) shellopts []string (default '') + sixel []string (default false) smartcase bool (default true) smartdia bool (default false) sortby string (default 'natural') @@ -905,6 +906,10 @@ Command line flag used to pass shell commands. List of shell options to pass to the shell executable. + sixel bool (default false) + +Render sixel images in preview + smartcase bool (default true) Override 'ignorecase' option when the pattern contains an uppercase character. diff --git a/lf.1 b/lf.1 index b0643f23..b29283d8 100644 --- a/lf.1 +++ b/lf.1 @@ -177,6 +177,7 @@ The following options can be used to customize the behavior of lf: shell string (default 'sh' for Unix and 'cmd' for Windows) shellflag string (default '-c' for Unix and '/c' for Windows) shellopts []string (default '') + sixel []string (default false) smartcase bool (default true) smartdia bool (default false) sortby string (default 'natural') @@ -1018,6 +1019,12 @@ Command line flag used to pass shell commands. .PP List of shell options to pass to the shell executable. .PP +.EX + sixel bool (default false) +.EE +.PP +Render sixel images in preview +.PP .EX smartcase bool (default true) .EE From 7535d379acc44b80efe96ed56bfbf74e0c686d5a Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Thu, 13 Apr 2023 15:39:30 +0200 Subject: [PATCH 11/24] refactor: use newSixelScreen() for init --- sixel.go | 5 +++-- ui.go | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sixel.go b/sixel.go index 81fb5b49..727b42c1 100644 --- a/sixel.go +++ b/sixel.go @@ -49,8 +49,9 @@ func (sxs *sixelScreen) filler(path string, l int) (fill string) { }() } - fill += strings.Repeat(string(gSixelFiller), l) - return fill +func newSixelScreen(wc, hc int) (sxs sixelScreen) { + sxs.updateSizes(wc, hc) + return sxs } func (sxs *sixelScreen) updateSizes(wc, hc int) { diff --git a/ui.go b/ui.go index f8d9f3ec..72300421 100644 --- a/ui.go +++ b/ui.go @@ -611,10 +611,9 @@ func newUI(screen tcell.Screen) *ui { styles: parseStyles(), icons: parseIcons(), currentFile: "", + sxScreen: newSixelScreen(screen.Size()), } - ui.sxScreen.updateSizes(screen.Size()) - go ui.pollEvents() return ui From dc883d2b72bc8e8ebd3c47817eccf86391580c7b Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Thu, 13 Apr 2023 15:59:28 +0200 Subject: [PATCH 12/24] fix bug where image is not always cleared --- sixel.go | 29 ++++++++++++----------------- ui.go | 7 +++++++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/sixel.go b/sixel.go index 727b42c1..076df73a 100644 --- a/sixel.go +++ b/sixel.go @@ -6,6 +6,8 @@ import ( "regexp" "strconv" "strings" + + "github.com/gdamore/tcell/v2" ) const ( @@ -26,7 +28,6 @@ type sixelScreen struct { wpx, hpx int fontw, fonth int sx []sixel - lastFile string altFill bool } @@ -35,19 +36,13 @@ func (sxs *sixelScreen) clear() { } // fillers are used to control when tcell redraws the region where a sixel image is drawn. -// alternating between bold("ESC [1m") and regular is to clear the image before drawing a new one. -func (sxs *sixelScreen) filler(path string, l int) (fill string) { - if path != sxs.lastFile { - sxs.altFill = !sxs.altFill - sxs.lastFile = path - } - +// alternating between bold and regular is to clear the image before drawing a new one. +func (sxs *sixelScreen) fillerStyle() tcell.Style { if sxs.altFill { - fill = "\033[1m" - defer func() { - fill += "\033[0m" - }() + return tcell.StyleDefault.Bold(true) } + return tcell.StyleDefault +} func newSixelScreen(wc, hc int) (sxs sixelScreen) { sxs.updateSizes(wc, hc) @@ -79,6 +74,8 @@ func (sxs *sixelScreen) showSixels() { } buf.WriteString("\0338") fmt.Print(buf.String()) + sxs.altFill = !sxs.altFill + } var reNumber = regexp.MustCompile(`^[0-9]+`) @@ -97,15 +94,13 @@ func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen yc := linenr maxh := (win.h - yc) * sxScreen.fonth s, hpx = trimSixelHeight(s, maxh) - wc, hc := sxScreen.pxToCells(wpx, hpx) - fill := sxScreen.filler(fpath, wc) + _, hc := sxScreen.pxToCells(wpx, hpx) - lines = append(lines, textbefore+fill) + lines = append(lines, textbefore) sixels = append(sixels, sixel{xc, yc, wpx, hpx, s}) - paddedfill := strings.Repeat(" ", xc-2) + fill for j := 1; j < hc; j++ { - lines = append(lines, paddedfill) + lines = append(lines, "") } linesAfter, sixelsAfter := renderPreviewLine(textafter, linenr, fpath, win, sxScreen) diff --git a/ui.go b/ui.go index 72300421..fa0f8bf8 100644 --- a/ui.go +++ b/ui.go @@ -275,7 +275,14 @@ func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool, sxs st = win.print(screen, 2, i, st, l) } + fill := sxs.fillerStyle() for _, sx := range reg.sixels { + wc, hc := sxs.pxToCells(sx.wPx, sx.hPx) + + for y := sx.y; y < sx.y+hc; y++ { + win.print(screen, sx.x, y, fill, strings.Repeat(string(gSixelFiller), min(wc, win.w-sx.x+2))) + } + s := sx s.x += win.x s.y += win.y From 6054a14a4b8899ad5fba8c8c22a390b2d923ca2b Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 14:18:39 +0200 Subject: [PATCH 13/24] refactor: separate sixel tests --- misc_test.go | 45 --------------------------------------------- sixel_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 sixel_test.go diff --git a/misc_test.go b/misc_test.go index 062d34bc..3568fa2f 100644 --- a/misc_test.go +++ b/misc_test.go @@ -243,48 +243,3 @@ func TestNaturalLess(t *testing.T) { } } } - -func TestTrimSixelHeight(t *testing.T) { - tests := []struct { - si string - hi int - so string - ho int - }{ - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 12, - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 6, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 30, - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 11, - "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 30, - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, - }, - { - "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 11, - "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, - }, - } - - for i, test := range tests { - if got1, got2 := trimSixelHeight(test.si, test.hi); got1 != test.so || got2 != test.ho { - t.Errorf("test #%d expected height %d, got %d and string %s", i, test.ho, got2, got1[1:]) - } - } - -} diff --git a/sixel_test.go b/sixel_test.go new file mode 100644 index 00000000..3367dc0e --- /dev/null +++ b/sixel_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "testing" +) + +func TestTrimSixelHeight(t *testing.T) { + tests := []struct { + si string + hi int + so string + ho int + }{ + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{\x1b\\", 6, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 30, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N\x1b\\", 11, + "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 30, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 12, + }, + { + "\x1bPq\"1;1;1;12#0;2;97;97;97#1;2;75;75;75#0B$#1{-#0o$#1N-\x1b\\", 11, + "\x1bPq\"1;1;1;6#0;2;97;97;97#1;2;75;75;75#0B$#1{-\x1b\\", 6, + }, + } + + for i, test := range tests { + if got1, got2, _ := trimSixelHeight(test.si, test.hi); got1 != test.so || got2 != test.ho { + t.Errorf("test #%d expected height %d, got %d and string %s", i, test.ho, got2, got1[1:]) + } + } + +} From 9b0de8d509410b5dd8fec54658c6acc0e27e7c84 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 14:19:39 +0200 Subject: [PATCH 14/24] improved error handling --- sixel.go | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/sixel.go b/sixel.go index 076df73a..9778e8f0 100644 --- a/sixel.go +++ b/sixel.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "log" "regexp" @@ -16,7 +17,8 @@ const ( ) var ( - gSixelFiller = '\u2800' + errInvalidSixel = errors.New("Invalid sixel sequence") + gSixelFiller = '\u2800' ) type sixel struct { @@ -87,13 +89,15 @@ func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen textbefore := text[:a] s := text[a : a+b+len(gSixelTerminate)] textafter := text[a+b+len(gSixelTerminate):] - wpx, hpx := sixelDimPx(s) + wpx, hpx, err := sixelDimPx(s) - if wpx >= 0 || hpx >= 0 { + if err == nil { xc := runeSliceWidth([]rune(textbefore)) + 2 yc := linenr maxh := (win.h - yc) * sxScreen.fonth - s, hpx = trimSixelHeight(s, maxh) + + // any syntax error should already be caught by sixelDimPx + s, hpx, _ = trimSixelHeight(s, maxh) _, hc := sxScreen.pxToCells(wpx, hpx) lines = append(lines, textbefore) @@ -115,7 +119,7 @@ func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen } // needs some testing -func sixelDimPx(s string) (w int, h int) { +func sixelDimPx(s string) (w int, h int, err error) { // TODO maybe take into account pixel aspect ratio // General sixel sequence: @@ -128,7 +132,7 @@ func sixelDimPx(s string) (w int, h int) { i := strings.Index(s, "q") + 1 if i == 0 { // syntax error - return -1, -1 + return 0, 0, errInvalidSixel } // Start of (optional) Raster Attributes @@ -159,7 +163,7 @@ func sixelDimPx(s string) (w int, h int) { // TODO // ph and pv are more like suggestions, it's still possible to go over the // reported size, so we might need to parse the entire main body anyway - return ph, pv + return ph, pv, nil } main_body: @@ -180,11 +184,11 @@ main_body: m := reNumber.FindString(s[i+1:]) if m == "" { // syntax error - return -1, -1 + return 0, 0, errInvalidSixel } if s[i+1+len(m)] < '?' || s[i+1+len(m)] > '~' { // syntax error - return -1, -1 + return 0, 0, errInvalidSixel } n, _ := strconv.Atoi(m) wi += n - 1 @@ -195,18 +199,18 @@ main_body: w = max(w, wi) h++ // add newline on last row } - return w, h * 6 + return w, h * 6, nil } // maybe merge with sixelDimPx() -func trimSixelHeight(s string, maxh int) (string, int) { +func trimSixelHeight(s string, maxh int) (seq string, trimmedHeight int, err error) { var h int maxh = maxh - (maxh % 6) i := strings.Index(s, "q") + 1 if i == 0 { // syntax error - return "", -1 + return "", -1, errInvalidSixel } if s[i] == '"' { @@ -241,12 +245,12 @@ func trimSixelHeight(s string, maxh int) (string, int) { } if i == 0 { - return s, 6 + return s, 6, nil } if len(s) > i+3 { - return s[:i+1] + "\x1b\\", h + return s[:i+1] + "\x1b\\", h, nil } - return s, h + return s, h, nil } From 49c98e06d7b650351de768742e52f5cf5146d6ce Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 14:36:12 +0200 Subject: [PATCH 15/24] rename & comments for clarity --- sixel.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/sixel.go b/sixel.go index 9778e8f0..17dcd43b 100644 --- a/sixel.go +++ b/sixel.go @@ -129,6 +129,8 @@ func sixelDimPx(s string) (w int, h int, err error) { // the optional raster attributes may contain the 'reported' image size in pixels // (The actual image can be larger, but is at least this big) // ST is the terminating string "ESC \" + // + // https://vt100.net/docs/vt3xx-gp/chapter14.html i := strings.Index(s, "q") + 1 if i == 0 { // syntax error @@ -167,20 +169,20 @@ func sixelDimPx(s string) (w int, h int, err error) { } main_body: - var wi int + var w_line int for ; i < len(s)-2; i++ { c := s[i] switch { - case '?' <= c && c <= '~': - wi++ - case c == '-': - w = max(w, wi) - wi = 0 + case '?' <= c && c <= '~': // data char + w_line++ + case c == '-': // next line + w = max(w, w_line) + w_line = 0 h++ - case c == '$': - w = max(w, wi) - wi = 0 - case c == '!': + case c == '$': // Graphics Carriage Return: go back to start of same line + w = max(w, w_line) + w_line = 0 + case c == '!': // Repeat Introducer m := reNumber.FindString(s[i+1:]) if m == "" { // syntax error @@ -191,12 +193,14 @@ main_body: return 0, 0, errInvalidSixel } n, _ := strconv.Atoi(m) - wi += n - 1 + w_line += n - 1 default: + // other cases: + // c == '#' (change color) } } if s[len(s)-3] != '-' { - w = max(w, wi) + w = max(w, w_line) h++ // add newline on last row } return w, h * 6, nil From 1e050e5e4251da88255b1f0316624a05ed84c7a1 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 14:53:07 +0200 Subject: [PATCH 16/24] refactor: renderPreviewLine should not use gOpts --- nav.go | 10 +++++++--- sixel.go | 55 +++++++++++++++++++++++++++---------------------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/nav.go b/nav.go index 244479de..df6acbf3 100644 --- a/nav.go +++ b/nav.go @@ -814,9 +814,13 @@ func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { } } - lines, sixels := renderPreviewLine(text, len(reg.lines), path, win, sxScreen) - reg.lines = append(reg.lines, lines...) - reg.sixels = append(reg.sixels, sixels...) + if gOpts.sixel && sxScreen.wpx > 0 && sxScreen.hpx > 0 { + lines, sixels := renderPreviewLine(text, len(reg.lines), path, win, sxScreen) + reg.lines = append(reg.lines, lines...) + reg.sixels = append(reg.sixels, sixels...) + } else { + reg.lines = append(reg.lines, text) + } } if buf.Err() != nil { diff --git a/sixel.go b/sixel.go index 17dcd43b..9f599b9f 100644 --- a/sixel.go +++ b/sixel.go @@ -83,38 +83,37 @@ func (sxs *sixelScreen) showSixels() { var reNumber = regexp.MustCompile(`^[0-9]+`) func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen *sixelScreen) (lines []string, sixels []sixel) { - if gOpts.sixel && sxScreen.wpx > 0 && sxScreen.hpx > 0 { - if a := strings.Index(text, gSixelBegin); a >= 0 { - if b := strings.Index(text[a:], gSixelTerminate); b >= 0 { - textbefore := text[:a] - s := text[a : a+b+len(gSixelTerminate)] - textafter := text[a+b+len(gSixelTerminate):] - wpx, hpx, err := sixelDimPx(s) - - if err == nil { - xc := runeSliceWidth([]rune(textbefore)) + 2 - yc := linenr - maxh := (win.h - yc) * sxScreen.fonth - - // any syntax error should already be caught by sixelDimPx - s, hpx, _ = trimSixelHeight(s, maxh) - _, hc := sxScreen.pxToCells(wpx, hpx) - - lines = append(lines, textbefore) - - sixels = append(sixels, sixel{xc, yc, wpx, hpx, s}) - for j := 1; j < hc; j++ { - lines = append(lines, "") - } - - linesAfter, sixelsAfter := renderPreviewLine(textafter, linenr, fpath, win, sxScreen) - lines = append(lines, linesAfter...) - sixels = append(sixels, sixelsAfter...) - return lines, sixels + if a := strings.Index(text, gSixelBegin); a >= 0 { + if b := strings.Index(text[a:], gSixelTerminate); b >= 0 { + textbefore := text[:a] + s := text[a : a+b+len(gSixelTerminate)] + textafter := text[a+b+len(gSixelTerminate):] + wpx, hpx, err := sixelDimPx(s) + + if err == nil { + xc := runeSliceWidth([]rune(textbefore)) + 2 + yc := linenr + maxh := (win.h - yc) * sxScreen.fonth + + // any syntax error should already be caught by sixelDimPx + s, hpx, _ = trimSixelHeight(s, maxh) + _, hc := sxScreen.pxToCells(wpx, hpx) + + lines = append(lines, textbefore) + + sixels = append(sixels, sixel{xc, yc, wpx, hpx, s}) + for j := 1; j < hc; j++ { + lines = append(lines, "") } + + linesAfter, sixelsAfter := renderPreviewLine(textafter, linenr, fpath, win, sxScreen) + lines = append(lines, linesAfter...) + sixels = append(sixels, sixelsAfter...) + return lines, sixels } } } + return []string{text}, sixels } From c4080d947af2c1cf39c4336e12a0e1ea46507fb5 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 15:08:53 +0200 Subject: [PATCH 17/24] rename for clarity --- sixel.go | 70 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/sixel.go b/sixel.go index 9f599b9f..9ef97b1d 100644 --- a/sixel.go +++ b/sixel.go @@ -23,7 +23,7 @@ var ( type sixel struct { x, y, wPx, hPx int - str string + data string } type sixelScreen struct { @@ -72,7 +72,7 @@ func (sxs *sixelScreen) showSixels() { buf.WriteString("\0337") for _, sixel := range sxs.sx { buf.WriteString(fmt.Sprintf("\033[%d;%dH", sixel.y+1, sixel.x+1)) - buf.WriteString(sixel.str) + buf.WriteString(sixel.data) } buf.WriteString("\0338") fmt.Print(buf.String()) @@ -86,22 +86,22 @@ func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen if a := strings.Index(text, gSixelBegin); a >= 0 { if b := strings.Index(text[a:], gSixelTerminate); b >= 0 { textbefore := text[:a] - s := text[a : a+b+len(gSixelTerminate)] + data := text[a : a+b+len(gSixelTerminate)] textafter := text[a+b+len(gSixelTerminate):] - wpx, hpx, err := sixelDimPx(s) + wpx, hpx, err := sixelDimPx(data) if err == nil { xc := runeSliceWidth([]rune(textbefore)) + 2 yc := linenr maxh := (win.h - yc) * sxScreen.fonth - // any syntax error should already be caught by sixelDimPx - s, hpx, _ = trimSixelHeight(s, maxh) + // any syntax error should already be caught by sixelDimPx, error is safe to discard + data, hpx, _ = trimSixelHeight(data, maxh) _, hc := sxScreen.pxToCells(wpx, hpx) lines = append(lines, textbefore) - sixels = append(sixels, sixel{xc, yc, wpx, hpx, s}) + sixels = append(sixels, sixel{xc, yc, wpx, hpx, data}) for j := 1; j < hc; j++ { lines = append(lines, "") } @@ -118,7 +118,7 @@ func renderPreviewLine(text string, linenr int, fpath string, win *win, sxScreen } // needs some testing -func sixelDimPx(s string) (w int, h int, err error) { +func sixelDimPx(data string) (w int, h int, err error) { // TODO maybe take into account pixel aspect ratio // General sixel sequence: @@ -130,7 +130,7 @@ func sixelDimPx(s string) (w int, h int, err error) { // ST is the terminating string "ESC \" // // https://vt100.net/docs/vt3xx-gp/chapter14.html - i := strings.Index(s, "q") + 1 + i := strings.Index(data, "q") + 1 if i == 0 { // syntax error return 0, 0, errInvalidSixel @@ -140,21 +140,21 @@ func sixelDimPx(s string) (w int, h int, err error) { // " Pan ; Pad; Ph; Pv // pixel aspect ratio = Pan/Pad // We are only interested in Ph and Pv (horizontal and vertical size in px) - if s[i] == '"' { + if data[i] == '"' { i++ - b := strings.Index(s[i:], ";") + b := strings.Index(data[i:], ";") // pan := strconv.Atoi(s[a:b]) i += b + 1 - b = strings.Index(s[i:], ";") + b = strings.Index(data[i:], ";") // pad := strconv.Atoi(s[a:b]) i += b + 1 - b = strings.Index(s[i:], ";") - ph, err1 := strconv.Atoi(s[i : i+b]) + b = strings.Index(data[i:], ";") + ph, err1 := strconv.Atoi(data[i : i+b]) i += b + 1 - b = strings.Index(s[i:], "#") - pv, err2 := strconv.Atoi(s[i : i+b]) + b = strings.Index(data[i:], "#") + pv, err2 := strconv.Atoi(data[i : i+b]) i += b if err1 != nil || err2 != nil { @@ -169,8 +169,8 @@ func sixelDimPx(s string) (w int, h int, err error) { main_body: var w_line int - for ; i < len(s)-2; i++ { - c := s[i] + for ; i < len(data)-2; i++ { + c := data[i] switch { case '?' <= c && c <= '~': // data char w_line++ @@ -182,12 +182,12 @@ main_body: w = max(w, w_line) w_line = 0 case c == '!': // Repeat Introducer - m := reNumber.FindString(s[i+1:]) + m := reNumber.FindString(data[i+1:]) if m == "" { // syntax error return 0, 0, errInvalidSixel } - if s[i+1+len(m)] < '?' || s[i+1+len(m)] > '~' { + if data[i+1+len(m)] < '?' || data[i+1+len(m)] > '~' { // syntax error return 0, 0, errInvalidSixel } @@ -198,7 +198,7 @@ main_body: // c == '#' (change color) } } - if s[len(s)-3] != '-' { + if data[len(data)-3] != '-' { w = max(w, w_line) h++ // add newline on last row } @@ -206,28 +206,28 @@ main_body: } // maybe merge with sixelDimPx() -func trimSixelHeight(s string, maxh int) (seq string, trimmedHeight int, err error) { +func trimSixelHeight(data string, maxh int) (res string, trimmedHeight int, err error) { var h int maxh = maxh - (maxh % 6) - i := strings.Index(s, "q") + 1 + i := strings.Index(data, "q") + 1 if i == 0 { // syntax error return "", -1, errInvalidSixel } - if s[i] == '"' { + if data[i] == '"' { i++ for j := 0; j < 3; j++ { - b := strings.Index(s[i:], ";") + b := strings.Index(data[i:], ";") i += b + 1 } - b := strings.Index(s[i:], "#") - pv, err := strconv.Atoi(s[i : i+b]) + b := strings.Index(data[i:], "#") + pv, err := strconv.Atoi(data[i : i+b]) if err == nil && pv > maxh { mh := strconv.Itoa(maxh) - s = s[:i] + mh + s[i+b:] + data = data[:i] + mh + data[i+b:] i += len(mh) } else { i += b @@ -235,11 +235,11 @@ func trimSixelHeight(s string, maxh int) (seq string, trimmedHeight int, err err } for h < maxh { - k := strings.IndexRune(s[i+1:], '-') + k := strings.IndexRune(data[i+1:], '-') if k == -1 { - if s[len(s)-3] != '-' { + if data[len(data)-3] != '-' { h += 6 - i = len(s) - 3 + i = len(data) - 3 } break } @@ -248,12 +248,12 @@ func trimSixelHeight(s string, maxh int) (seq string, trimmedHeight int, err err } if i == 0 { - return s, 6, nil + return data, 6, nil } - if len(s) > i+3 { - return s[:i+1] + "\x1b\\", h, nil + if len(data) > i+3 { + return data[:i+1] + "\x1b\\", h, nil } - return s, h, nil + return data, h, nil } From 53c3be01aa394e942ed1f45d306b54dec1f9c15a Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Fri, 14 Apr 2023 16:30:26 +0200 Subject: [PATCH 18/24] fix: filler style changed more than needed The filler style now only changes when the file in preview is changed --- sixel.go | 9 ++++++--- ui.go | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sixel.go b/sixel.go index 9ef97b1d..89167d2a 100644 --- a/sixel.go +++ b/sixel.go @@ -31,6 +31,7 @@ type sixelScreen struct { fontw, fonth int sx []sixel altFill bool + lastFile string // TODO maybe use hash of sixels instead to flip altFill } func (sxs *sixelScreen) clear() { @@ -39,7 +40,11 @@ func (sxs *sixelScreen) clear() { // 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. -func (sxs *sixelScreen) fillerStyle() tcell.Style { +func (sxs *sixelScreen) fillerStyle(filePath string) tcell.Style { + if sxs.lastFile != filePath { + sxs.altFill = !sxs.altFill + } + if sxs.altFill { return tcell.StyleDefault.Bold(true) } @@ -76,8 +81,6 @@ func (sxs *sixelScreen) showSixels() { } buf.WriteString("\0338") fmt.Print(buf.String()) - sxs.altFill = !sxs.altFill - } var reNumber = regexp.MustCompile(`^[0-9]+`) diff --git a/ui.go b/ui.go index fa0f8bf8..2add3833 100644 --- a/ui.go +++ b/ui.go @@ -275,7 +275,7 @@ func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool, sxs st = win.print(screen, 2, i, st, l) } - fill := sxs.fillerStyle() + fill := sxs.fillerStyle(reg.path) for _, sx := range reg.sixels { wc, hc := sxs.pxToCells(sx.wPx, sx.hPx) @@ -1036,6 +1036,7 @@ func (ui *ui) draw(nav *nav) { ui.screen.Show() if ui.menuBuf == nil && ui.cmdPrefix == "" && len(ui.sxScreen.sx) > 0 { + ui.sxScreen.lastFile = ui.regPrev.path ui.sxScreen.showSixels() } From eb530de131729a1fb9c33027ac18a07a0b9aeff1 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sat, 15 Apr 2023 22:36:23 +0200 Subject: [PATCH 19/24] rename --- nav.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nav.go b/nav.go index df6acbf3..d03aaf08 100644 --- a/nav.go +++ b/nav.go @@ -21,7 +21,7 @@ import ( type linkState byte const ( - previewerMaxLineSize int = 1048576 // 1 MB + gPreviewerMaxLineSize int = 1048576 // 1 MB ) const ( From dd3b81ae5e1c4a94d4c4aee76de7ccd3c20136d6 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sat, 15 Apr 2023 22:38:00 +0200 Subject: [PATCH 20/24] fix: forgot to rename this --- nav.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nav.go b/nav.go index d03aaf08..2c5cd00d 100644 --- a/nav.go +++ b/nav.go @@ -803,7 +803,7 @@ func (nav *nav) preview(path string, sxScreen *sixelScreen, win *win) { } buf := bufio.NewScanner(reader) - buf.Buffer(make([]byte, 0), previewerMaxLineSize) + buf.Buffer(make([]byte, 0), gPreviewerMaxLineSize) for i := 0; len(reg.lines) < win.h && buf.Scan(); i++ { text := buf.Text() From c77478cede078a167cd06c455a91189eccce73f6 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sat, 15 Apr 2023 22:48:47 +0200 Subject: [PATCH 21/24] increase max line size --- nav.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nav.go b/nav.go index 2c5cd00d..957cd6d2 100644 --- a/nav.go +++ b/nav.go @@ -21,7 +21,7 @@ import ( type linkState byte const ( - gPreviewerMaxLineSize int = 1048576 // 1 MB + gPreviewerMaxLineSize int = 2097152 // 2 MB ) const ( From 0270a4ccae90ffb82ea55800ccf3c4164866c3eb Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sat, 15 Apr 2023 23:03:06 +0200 Subject: [PATCH 22/24] remove unneeded reassignment --- nav.go | 1 - 1 file changed, 1 deletion(-) diff --git a/nav.go b/nav.go index 957cd6d2..f5517ff0 100644 --- a/nav.go +++ b/nav.go @@ -665,7 +665,6 @@ func (nav *nav) previewLoop(ui *ui) { nav.volatilePreview = false } if len(path) != 0 { - win := ui.wins[len(ui.wins)-1] nav.preview(path, &ui.sxScreen, win) prev = path } From 22cea203f263e556eac755d4502b6eb4e0097f99 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang <59727193+horriblename@users.noreply.github.com> Date: Mon, 1 May 2023 13:42:02 +0200 Subject: [PATCH 23/24] Update doc.go Co-authored-by: Joe Lim <50560759+joelim-work@users.noreply.github.com> --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index eb7ad8f2..e671f17e 100644 --- a/doc.go +++ b/doc.go @@ -851,7 +851,7 @@ List of shell options to pass to the shell executable. sixel bool (default false) -Render sixel images in preview +Render sixel images in preview. smartcase bool (default true) From 35096133a401d845801628ea0e2be018baf16a51 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang <59727193+horriblename@users.noreply.github.com> Date: Mon, 1 May 2023 13:42:25 +0200 Subject: [PATCH 24/24] fix typo Co-authored-by: Joe Lim <50560759+joelim-work@users.noreply.github.com> --- eval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eval.go b/eval.go index fa1db036..8d4a00f7 100644 --- a/eval.go +++ b/eval.go @@ -868,7 +868,7 @@ func (e *setExpr) eval(app *app, args []string) { app.ui.echoerrf("sixel!: unexpected value: %s", e.val) return } - gOpts.wrapscroll = !gOpts.wrapscroll + gOpts.sixel= !gOpts.sixel default: // any key with the prefix user_ is accepted as a user defined option if strings.HasPrefix(e.opt, "user_") {