Skip to content

Commit

Permalink
sixel preview (gokcehan#1211)
Browse files Browse the repository at this point in the history
* Sixel patch (gokcehan#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 <sixel sequence> 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

* fix comment

* refactor: remove nested if-block

* refactor: move sixel related stuff into sixel.go

* remove unused function

* allow long lines from previewer

* rework preview to accept only single-line sixels

- multi-line support removed for simplicity in the code base

* add new option `sixel`

* add eval logic and completion of `sixel` option

* doc: new option 'sixel'

* refactor: use newSixelScreen() for init

* fix bug where image is not always cleared

* refactor: separate sixel tests

* improved error handling

* rename & comments for clarity

* refactor: renderPreviewLine should not use gOpts

* rename for clarity

* fix: filler style changed more than needed

The filler style now only changes when the file in preview is changed

* rename

* fix: forgot to rename this

* increase max line size

* remove unneeded reassignment

* fix position of multiple sixel on same line

* refactor: extract code into function

* update docs

* reorder variable

* remove unneeded var

* avoid unneeded ioctl calls

* fixup! fix typo

* rework/simplify previewing sixels

Remove support for multiple sixels on the same line, and the line must start with 'ESC P' to be considered a sixel.

* separate sixel detection logic from text scanning

* rework: stricter sixel validation and remove trimming

* revert unneeded changes

* strip validation logic

* refactor: use singular sixel instead of slice

multiple sixel preview is no longer supported

* fix: bug where first 2 char of non-sixel is missing

* docs: fix wrong option type

* set sixelScreen.wpx, hpx to 0 on error

* remove terminal px size

* fix: bug where quitting might leave terminal in bold

cleanup

* update docs

* remove unneeded sixel type

* cleanup unused func argument

* cleanup unneeded function

* refactor: more readable

Co-authored-by: Joe Lim <50560759+joelim-work@users.noreply.github.com>

* cleanup unneeded var

* cleanup unneeded constructor

* cleanup unused func arg

---------

Co-authored-by: Joe Lim <50560759+joelim-work@users.noreply.github.com>
  • Loading branch information
horriblename and joelim-work authored Aug 12, 2023
1 parent b2f8673 commit dd82949
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 2 deletions.
3 changes: 3 additions & 0 deletions complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ var (
"reverse!",
"ruler",
"preserve",
"sixel",
"nosixel",
"sixel!",
"smartcase",
"nosmartcase",
"smartcase!",
Expand Down
5 changes: 5 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,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 bool (default false)
smartcase bool (default true)
smartdia bool (default false)
sortby string (default 'natural')
Expand Down Expand Up @@ -910,6 +911,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.
Expand Down
5 changes: 5 additions & 0 deletions docstring.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,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.sixel = !gOpts.sixel
default:
// any key with the prefix user_ is accepted as a user defined option
if strings.HasPrefix(e.opt, "user_") {
Expand Down
7 changes: 7 additions & 0 deletions lf.1
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,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 bool (default false)
smartcase bool (default true)
smartdia bool (default false)
sortby string (default 'natural')
Expand Down Expand Up @@ -1083,6 +1084,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
Expand Down
17 changes: 17 additions & 0 deletions nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -847,6 +848,22 @@ func (nav *nav) preview(path string, win *win) {
reader = f
}

prefix := make([]byte, 2)
if gOpts.sixel {
_, err := reader.Read(prefix)
reader = io.MultiReader(bytes.NewReader(prefix), reader)

if err == nil && string(prefix) == gSixelBegin {
b, err := io.ReadAll(reader)
if err != nil {
log.Printf("loading sixel: %s", err)
}
str := string(b)
reg.sixel = &str
return
}
}

buf := bufio.NewScanner(reader)

for i := 0; i < win.h && buf.Scan(); i++ {
Expand Down
2 changes: 2 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var gOpts struct {
mouse bool
number bool
preview bool
sixel bool
relativenumber bool
smartcase bool
smartdia bool
Expand Down Expand Up @@ -113,6 +114,7 @@ func init() {
gOpts.mouse = false
gOpts.number = false
gOpts.preview = true
gOpts.sixel = false
gOpts.relativenumber = false
gOpts.smartcase = true
gOpts.smartdia = false
Expand Down
61 changes: 61 additions & 0 deletions sixel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"fmt"
"strings"

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

const (
gSixelBegin = "\033P"
gSixelFiller = '\u2800'
)

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

func (sxs *sixelScreen) fillerStyle(filePath string) tcell.Style {
if sxs.lastFile != filePath {
sxs.altFill = !sxs.altFill
}

if sxs.altFill {
return tcell.StyleDefault.Bold(true)
}
return tcell.StyleDefault
}

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

// XXX: workaround for bug where quitting lf might leave the terminal in bold
fmt.Print("\033[0m")

fmt.Print("\0337") // Save cursor position
fmt.Printf("\033[%d;%dH", sxs.yprev, sxs.xprev) // Move cursor to position
fmt.Print(*sxs.sixel) //
fmt.Print("\0338") // Restore cursor position
}

func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) {
if reg.sixel == nil {
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 := 0; y < win.h; y++ {
st = win.print(screen, 0, y, st, strings.Repeat(string(gSixelFiller), win.w))
}

sxs.xprev, sxs.yprev = win.x+1, win.y+1
sxs.sixel = reg.sixel
}
15 changes: 13 additions & 2 deletions ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -274,6 +274,8 @@ func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool) {

st = win.print(screen, 2, i, st, l)
}

sxs.printSixel(win, screen, reg)
}

var gThisYear = time.Now().Year()
Expand Down Expand Up @@ -558,6 +560,7 @@ func getWins(screen tcell.Screen) []*win {

type ui struct {
screen tcell.Screen
sxScreen sixelScreen
polling bool
wins []*win
promptWin *win
Expand Down Expand Up @@ -600,6 +603,7 @@ func newUI(screen tcell.Screen) *ui {
styles: parseStyles(),
icons: parseIcons(),
currentFile: "",
sxScreen: sixelScreen{},
}

go ui.pollEvents()
Expand Down Expand Up @@ -679,6 +683,7 @@ type reg struct {
loadTime time.Time
path string
lines []string
sixel *string
}

func (ui *ui) loadFile(app *app, volatile bool) {
Expand Down Expand Up @@ -955,6 +960,7 @@ func (ui *ui) draw(nav *nav) {
ui.screen.SetContent(i, j, ' ', nil, st)
}
}
ui.sxScreen.sixel = nil

ui.drawPromptLine(nav)

Expand Down Expand Up @@ -1003,7 +1009,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)
}
}
}
Expand Down Expand Up @@ -1033,6 +1039,11 @@ 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) {
Expand Down

0 comments on commit dd82949

Please sign in to comment.