Add ability to use image previewers (#531)

* Return early on error in nav.preview

* nav.checkReg now returns a boolean instead of calling nav.preview

* Pass width, height, x, and y parameters to preview script

* Check previewer for exit code

If non-zero the preview will be assumed to have side-effects.

* Add the cleaner option

This is called upon selection changes if the previous preview was
volatile. To this end, volatilePreview was added to the nav struct
This commit is contained in:
neeshy 2020-12-24 13:13:20 +00:00 committed by GitHub
parent c2fe88e9a0
commit 82f03102a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 98 additions and 25 deletions

8
app.go
View File

@ -213,6 +213,8 @@ func (app *app) loop() {
continue continue
} }
go app.nav.previewClear()
log.Print("bye!") log.Print("bye!")
if err := app.writeHistory(); err != nil { if err := app.writeHistory(); err != nil {
@ -310,7 +312,10 @@ func (app *app) loop() {
app.ui.draw(app.nav) app.ui.draw(app.nav)
case r := <-app.nav.regChan: case r := <-app.nav.regChan:
app.nav.checkReg(r) if app.nav.checkReg(r) {
win := app.ui.wins[len(app.ui.wins)-1]
go app.nav.preview(r.path, win)
}
app.nav.regCache[r.path] = r app.nav.regCache[r.path] = r
@ -402,6 +407,7 @@ func (app *app) runShell(s string, args []string, prefix string) {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
go app.nav.previewClear()
app.ui.pause() app.ui.pause()
defer app.ui.resume() defer app.ui.resume()
defer app.nav.renew() defer app.nav.renew()

View File

@ -131,6 +131,7 @@ var (
"ifs", "ifs",
"info", "info",
"previewer", "previewer",
"cleaner",
"promptfmt", "promptfmt",
"ratios", "ratios",
"shell", "shell",

10
doc.go
View File

@ -118,6 +118,7 @@ The following options can be used to customize the behavior of lf:
period int (default 0) period int (default 0)
preview bool (default on) preview bool (default on)
previewer string (default '') previewer string (default '')
cleaner string (default '')
promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m") promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m")
ratios []int (default '1:2:3') ratios []int (default '1:2:3')
relativenumber bool (default off) relativenumber bool (default off)
@ -591,10 +592,17 @@ Files containing the null character (U+0000) in the read portion are considered
Set the path of a previewer file to filter the content of regular files for previewing. Set the path of a previewer file to filter the content of regular files for previewing.
The file should be executable. The file should be executable.
Two arguments are passed to the file, first is the current file name, and second is the height of preview pane. Five arguments are passed to the file, first is the current file name; the second, third, fourth, and fifth are height, width, horizontal position, and vertical position of preview pane respectively.
SIGPIPE signal is sent when enough lines are read. SIGPIPE signal is sent when enough lines are read.
If the previewer returns a non-zero exit code, then the preview cache for the given file is disabled. This means that if the file is selected in the future, the previewer is called once again.
Preview filtering is disabled and files are displayed as they are when the value of this option is left empty. Preview filtering is disabled and files are displayed as they are when the value of this option is left empty.
cleaner string (default '') (not called if empty)
Set the path of a cleaner file. This file will be called if previewing is enabled, the previewer is set, and the previously selected file had its preview cache disabled.
The file should be executable.
Preview clearing is disabled when the value of this option is left empty.
promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w/\033[0m\033[1m%f\033[0m") promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w/\033[0m\033[1m%f\033[0m")
Format string of the prompt shown in the top line. Format string of the prompt shown in the top line.

View File

@ -121,6 +121,7 @@ The following options can be used to customize the behavior of lf:
period int (default 0) period int (default 0)
preview bool (default on) preview bool (default on)
previewer string (default '') previewer string (default '')
cleaner string (default '')
promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m") promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m")
ratios []int (default '1:2:3') ratios []int (default '1:2:3')
relativenumber bool (default off) relativenumber bool (default off)
@ -624,11 +625,21 @@ binary files and displayed as 'binary'.
previewer string (default '') (not filtered if empty) previewer string (default '') (not filtered if empty)
Set the path of a previewer file to filter the content of regular files for Set the path of a previewer file to filter the content of regular files for
previewing. The file should be executable. Two arguments are passed to the previewing. The file should be executable. Five arguments are passed to the
file, first is the current file name, and second is the height of preview file, first is the current file name; the second, third, fourth, and fifth
pane. SIGPIPE signal is sent when enough lines are read. Preview filtering are height, width, horizontal position, and vertical position of preview
is disabled and files are displayed as they are when the value of this pane respectively. SIGPIPE signal is sent when enough lines are read. If the
option is left empty. previewer returns a non-zero exit code, then the preview cache for the given
file is disabled. This means that if the file is selected in the future, the
previewer is called once again. Preview filtering is disabled and files are
displayed as they are when the value of this option is left empty.
cleaner string (default '') (not called if empty)
Set the path of a cleaner file. This file will be called if previewing is
enabled, the previewer is set, and the previously selected file had its
preview cache disabled. The file should be executable. Preview clearing is
disabled when the value of this option is left empty.
promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w/\033[0m\033[1m%f\033[0m") promptfmt string (default "\033[32;1m%u@%h\033[0m:\033[34;1m%w/\033[0m\033[1m%f\033[0m")

View File

@ -263,6 +263,8 @@ func (e *setExpr) eval(app *app, args []string) {
gOpts.info = toks gOpts.info = toks
case "previewer": case "previewer":
gOpts.previewer = replaceTilde(e.val) gOpts.previewer = replaceTilde(e.val)
case "cleaner":
gOpts.cleaner = replaceTilde(e.val)
case "promptfmt": case "promptfmt":
gOpts.promptfmt = e.val gOpts.promptfmt = e.val
case "ratios": case "ratios":

9
lf.1
View File

@ -132,6 +132,7 @@ The following options can be used to customize the behavior of lf:
period int (default 0) period int (default 0)
preview bool (default on) preview bool (default on)
previewer string (default '') previewer string (default '')
cleaner string (default '')
promptfmt string (default "\e033[32;1m%u@%h\e033[0m:\e033[34;1m%w\e033[0m\e033[1m%f\e033[0m") promptfmt string (default "\e033[32;1m%u@%h\e033[0m:\e033[34;1m%w\e033[0m\e033[1m%f\e033[0m")
ratios []int (default '1:2:3') ratios []int (default '1:2:3')
relativenumber bool (default off) relativenumber bool (default off)
@ -712,7 +713,13 @@ Show previews of files and directories at the right most pane. If the file has m
previewer string (default '') (not filtered if empty) previewer string (default '') (not filtered if empty)
.EE .EE
.PP .PP
Set the path of a previewer file to filter the content of regular files for previewing. The file should be executable. Two arguments are passed to the file, first is the current file name, and second is the height of preview pane. SIGPIPE signal is sent when enough lines are read. Preview filtering is disabled and files are displayed as they are when the value of this option is left empty. Set the path of a previewer file to filter the content of regular files for previewing. The file should be executable. Five arguments are passed to the file, first is the current file name; the second, third, fourth, and fifth are height, width, horizontal position, and vertical position of preview pane respectively. SIGPIPE signal is sent when enough lines are read. If the previewer returns a non-zero exit code, then the preview cache for the given file is disabled. This means that if the file is selected in the future, the previewer is called once again. Preview filtering is disabled and files are displayed as they are when the value of this option is left empty.
.PP
.EX
cleaner string (default '') (not called if empty)
.EE
.PP
Set the path of a cleaner file. This file will be called if previewing is enabled, the previewer is set, and the previously selected file had its preview cache disabled. The file should be executable. Preview clearing is disabled when the value of this option is left empty.
.PP .PP
.EX .EX
promptfmt string (default "\e033[32;1m%u@%h\e033[0m:\e033[34;1m%w/\e033[0m\e033[1m%f\e033[0m") promptfmt string (default "\e033[32;1m%u@%h\e033[0m:\e033[34;1m%w/\e033[0m\e033[1m%f\e033[0m")

65
nav.go
View File

@ -302,6 +302,7 @@ type nav struct {
searchBack bool searchBack bool
searchInd int searchInd int
searchPos int searchPos int
volatilePreview bool
} }
func (nav *nav) loadDir(path string) *dir { func (nav *nav) loadDir(path string) *dir {
@ -456,31 +457,51 @@ func (nav *nav) position() {
} }
} }
func (nav *nav) preview(path string) { func (nav *nav) preview(path string, win *win) {
reg := &reg{loadTime: time.Now(), path: path} reg := &reg{loadTime: time.Now(), path: path}
defer func() { nav.regChan <- reg }()
var reader io.Reader var reader io.Reader
if len(gOpts.previewer) != 0 { if len(gOpts.previewer) != 0 {
exportOpts() exportOpts()
cmd := exec.Command(gOpts.previewer, path, strconv.Itoa(nav.height)) cmd := exec.Command(gOpts.previewer, path,
strconv.Itoa(win.w),
strconv.Itoa(win.h),
strconv.Itoa(win.x),
strconv.Itoa(win.y))
out, err := cmd.StdoutPipe() out, err := cmd.StdoutPipe()
if err != nil { if err != nil {
log.Printf("previewing file: %s", err) log.Printf("previewing file: %s", err)
return
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Printf("previewing file: %s", err) log.Printf("previewing file: %s", err)
out.Close()
return
} }
defer cmd.Wait() defer func() {
if err := cmd.Wait(); err != nil {
if e, ok := err.(*exec.ExitError); ok {
if e.ExitCode() != 0 {
nav.volatilePreview = true
reg.volatile = true
}
} else {
log.Printf("loading file: %s", err)
}
}
}()
defer out.Close() defer out.Close()
reader = out reader = out
} else { } else {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
log.Printf("opening file: %s", err) log.Printf("opening file: %s", err)
return
} }
defer f.Close() defer f.Close()
@ -489,11 +510,10 @@ func (nav *nav) preview(path string) {
buf := bufio.NewScanner(reader) buf := bufio.NewScanner(reader)
for i := 0; i < nav.height && buf.Scan(); i++ { for i := 0; i < win.h && buf.Scan(); i++ {
for _, r := range buf.Text() { for _, r := range buf.Text() {
if r == 0 { if r == 0 {
reg.lines = []string{"\033[7mbinary\033[0m"} reg.lines = []string{"\033[7mbinary\033[0m"}
nav.regChan <- reg
return return
} }
} }
@ -503,28 +523,39 @@ func (nav *nav) preview(path string) {
if buf.Err() != nil { if buf.Err() != nil {
log.Printf("loading file: %s", buf.Err()) log.Printf("loading file: %s", buf.Err())
} }
nav.regChan <- reg
} }
func (nav *nav) loadReg(path string) *reg { func (nav *nav) previewClear() {
if len(gOpts.cleaner) != 0 && nav.volatilePreview {
nav.volatilePreview = false
cmd := exec.Command(gOpts.cleaner)
if err := cmd.Run(); err != nil {
log.Printf("cleaning preview: %s", err)
}
}
}
func (nav *nav) loadReg(path string, win *win) *reg {
r, ok := nav.regCache[path] r, ok := nav.regCache[path]
if !ok { if !ok || r.volatile {
r := &reg{loading: true, loadTime: time.Now(), path: path} r := &reg{loading: true, loadTime: time.Now(), path: path, volatile: true}
nav.regCache[path] = r nav.regCache[path] = r
go nav.preview(path) go nav.preview(path, win)
return r return r
} }
nav.checkReg(r) if nav.checkReg(r) {
go nav.preview(path, win)
}
return r return r
} }
func (nav *nav) checkReg(reg *reg) { func (nav *nav) checkReg(reg *reg) bool {
s, err := os.Stat(reg.path) s, err := os.Stat(reg.path)
if err != nil { if err != nil {
return return false
} }
now := time.Now() now := time.Now()
@ -532,13 +563,15 @@ func (nav *nav) checkReg(reg *reg) {
// XXX: Linux builtin exFAT drivers are able to predict modifications in the future // XXX: Linux builtin exFAT drivers are able to predict modifications in the future
// https://bugs.launchpad.net/ubuntu/+source/ubuntu-meta/+bug/1872504 // https://bugs.launchpad.net/ubuntu/+source/ubuntu-meta/+bug/1872504
if s.ModTime().After(now) { if s.ModTime().After(now) {
return return false
} }
if s.ModTime().After(reg.loadTime) { if s.ModTime().After(reg.loadTime) {
reg.loadTime = now reg.loadTime = now
go nav.preview(reg.path) return true
} }
return false
} }
func (nav *nav) sort() { func (nav *nav) sort() {

View File

@ -51,6 +51,7 @@ var gOpts struct {
filesep string filesep string
ifs string ifs string
previewer string previewer string
cleaner string
promptfmt string promptfmt string
shell string shell string
timefmt string timefmt string
@ -89,6 +90,7 @@ func init() {
gOpts.filesep = "\n" gOpts.filesep = "\n"
gOpts.ifs = "" gOpts.ifs = ""
gOpts.previewer = "" gOpts.previewer = ""
gOpts.cleaner = ""
gOpts.promptfmt = "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m" gOpts.promptfmt = "\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[1m%f\033[0m"
gOpts.shell = gDefaultShell gOpts.shell = gDefaultShell
gOpts.timefmt = time.ANSIC gOpts.timefmt = time.ANSIC

5
ui.go
View File

@ -609,6 +609,7 @@ func (ui *ui) echoerrf(format string, a ...interface{}) {
type reg struct { type reg struct {
loading bool loading bool
volatile bool
loadTime time.Time loadTime time.Time
path string path string
lines []string lines []string
@ -624,10 +625,12 @@ func (ui *ui) loadFile(nav *nav) {
return return
} }
go nav.previewClear()
if curr.IsDir() { if curr.IsDir() {
ui.dirPrev = nav.loadDir(curr.path) ui.dirPrev = nav.loadDir(curr.path)
} else if curr.Mode().IsRegular() { } else if curr.Mode().IsRegular() {
ui.regPrev = nav.loadReg(curr.path) win := ui.wins[len(ui.wins)-1]
ui.regPrev = nav.loadReg(curr.path, win)
} }
} }