lf/ui.go
KenjiTakahashi ef340a533d Do binary check after calling previewer
Many files valid for previewer, such as archives, are treated as binary
and thus were not passed to the previewer.

Also sanitizes the previewer output, to some extent.
2016-09-01 21:11:03 +02:00

750 lines
17 KiB
Go

package main
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"path"
"sort"
"strconv"
"strings"
"text/tabwriter"
"time"
"unicode"
"unicode/utf8"
"github.com/nsf/termbox-go"
)
const EscapeCode = 27
type Win struct {
w int
h int
x int
y int
}
func newWin(w, h, x, y int) *Win {
return &Win{w, h, x, y}
}
func (win *Win) renew(w, h, x, y int) {
win.w = w
win.h = h
win.x = x
win.y = y
}
func (win *Win) print(x, y int, fg, bg termbox.Attribute, s string) {
off := x
for i := 0; i < len(s); i++ {
r, w := utf8.DecodeRuneInString(s[i:])
if r == EscapeCode {
i++
if s[i] == '[' {
j := strings.IndexByte(s[i:], 'm')
toks := strings.Split(s[i+1:i+j], ";")
var nums []int
for _, t := range toks {
if t == "" {
fg = termbox.ColorDefault
bg = termbox.ColorDefault
break
}
i, err := strconv.Atoi(t)
if err != nil {
log.Printf("converting escape code: %s", err)
continue
}
nums = append(nums, i)
}
for _, n := range nums {
if 30 <= n && n <= 37 {
fg = termbox.ColorDefault
}
if 40 <= n && n <= 47 {
bg = termbox.ColorDefault
}
}
for _, n := range nums {
switch n {
case 1:
fg = fg | termbox.AttrBold
case 4:
fg = fg | termbox.AttrUnderline
case 7:
fg = fg | termbox.AttrReverse
case 30:
fg = fg | termbox.ColorBlack
case 31:
fg = fg | termbox.ColorRed
case 32:
fg = fg | termbox.ColorGreen
case 33:
fg = fg | termbox.ColorYellow
case 34:
fg = fg | termbox.ColorBlue
case 35:
fg = fg | termbox.ColorMagenta
case 36:
fg = fg | termbox.ColorCyan
case 37:
fg = fg | termbox.ColorWhite
case 40:
bg = bg | termbox.ColorBlack
case 41:
bg = bg | termbox.ColorRed
case 42:
bg = bg | termbox.ColorGreen
case 43:
bg = bg | termbox.ColorYellow
case 44:
bg = bg | termbox.ColorBlue
case 45:
bg = bg | termbox.ColorMagenta
case 46:
bg = bg | termbox.ColorCyan
case 47:
bg = bg | termbox.ColorWhite
}
}
i = i + j
continue
}
}
if x >= win.w {
break
}
termbox.SetCell(win.x+x, win.y+y, r, fg, bg)
i += w - 1
if r == '\t' {
x += gOpts.tabstop - (x-off)%gOpts.tabstop
} else {
x++
}
}
}
func (win *Win) printf(x, y int, fg, bg termbox.Attribute, format string, a ...interface{}) {
win.print(x, y, fg, bg, fmt.Sprintf(format, a...))
}
func (win *Win) printl(x, y int, fg, bg termbox.Attribute, s string) {
win.printf(x, y, fg, bg, "%s%*s", s, win.w-len(s), "")
}
func (win *Win) printd(dir *Dir, marks map[string]bool) {
if win.w < 3 {
return
}
fg, bg := termbox.ColorDefault, termbox.ColorDefault
if len(dir.fi) == 0 {
fg = termbox.AttrBold
win.print(0, 0, fg, bg, "empty")
return
}
maxind := len(dir.fi) - 1
beg := max(dir.ind-dir.pos, 0)
end := min(beg+win.h, maxind+1)
for i, f := range dir.fi[beg:end] {
switch {
case f.Mode().IsRegular():
if f.Mode()&0111 != 0 {
fg = termbox.AttrBold | termbox.ColorGreen
} else {
fg = termbox.ColorDefault
}
case f.Mode().IsDir():
fg = termbox.AttrBold | termbox.ColorBlue
case f.Mode()&os.ModeSymlink != 0:
fg = termbox.ColorCyan
case f.Mode()&os.ModeNamedPipe != 0:
fg = termbox.ColorRed
case f.Mode()&os.ModeSocket != 0:
fg = termbox.ColorYellow
case f.Mode()&os.ModeDevice != 0:
fg = termbox.ColorWhite
}
path := path.Join(dir.path, f.Name())
if marks[path] {
win.print(0, i, fg, termbox.ColorMagenta, " ")
}
if i == dir.pos {
fg = fg | termbox.AttrReverse
}
var s []byte
s = append(s, ' ')
s = append(s, f.Name()...)
if len(s) > win.w-2 {
s = s[:win.w-2]
} else {
s = append(s, make([]byte, win.w-2-len(s))...)
}
switch gOpts.showinfo {
case "none":
break
case "size":
if win.w > 8 {
h := humanize(f.Size())
s = append(s[:win.w-3-len(h)])
s = append(s, ' ')
s = append(s, h...)
}
case "time":
if win.w > 24 {
t := f.ModTime().Format("Jan _2 15:04")
s = append(s[:win.w-3-len(t)])
s = append(s, ' ')
s = append(s, t...)
}
default:
log.Printf("unknown showinfo type: %s", gOpts.showinfo)
}
// TODO: add a trailing '~' to the name if cut
win.print(1, i, fg, bg, string(s))
}
}
func (win *Win) printr(reg *os.File) error {
var reader io.ReadSeeker
if len(gOpts.previewer) != 0 {
cmd := exec.Command(gOpts.previewer, reg.Name(), strconv.Itoa(win.w), strconv.Itoa(win.h))
out, err := cmd.Output()
if err != nil {
log.Printf("previewing file: %s", err)
}
reader = bytes.NewReader(out)
} else {
reader = reg
}
fg, bg := termbox.ColorDefault, termbox.ColorDefault
buf := bufio.NewScanner(reader)
for i := 0; i < win.h && buf.Scan(); i++ {
for _, r := range buf.Text() {
if unicode.IsSpace(r) {
continue
}
if !unicode.IsPrint(r) && r != EscapeCode {
fg = termbox.AttrBold
win.print(0, 0, fg, bg, "binary")
return nil
}
}
}
if buf.Err() != nil {
return fmt.Errorf("printing regular file: %s", buf.Err())
}
reader.Seek(0, 0)
buf = bufio.NewScanner(reader)
for i := 0; i < win.h && buf.Scan(); i++ {
win.print(2, i, fg, bg, buf.Text())
}
if buf.Err() != nil {
return fmt.Errorf("printing regular file: %s", buf.Err())
}
return nil
}
type UI struct {
wins []*Win
pwdwin *Win
msgwin *Win
menuwin *Win
message string
}
func getWidths(wtot int) []int {
rsum := 0
for _, rat := range gOpts.ratios {
rsum += rat
}
wlen := len(gOpts.ratios)
widths := make([]int, wlen)
wsum := 0
for i := 0; i < wlen-1; i++ {
widths[i] = gOpts.ratios[i] * (wtot / rsum)
wsum += widths[i]
}
widths[wlen-1] = wtot - wsum
return widths
}
func newUI() *UI {
wtot, htot := termbox.Size()
var wins []*Win
widths := getWidths(wtot)
wacc := 0
wlen := len(widths)
for i := 0; i < wlen; i++ {
wins = append(wins, newWin(widths[i], htot-2, wacc, 1))
wacc += widths[i]
}
return &UI{
wins: wins,
pwdwin: newWin(wtot, 1, 0, 0),
msgwin: newWin(wtot, 1, 0, htot-1),
menuwin: newWin(wtot, 1, 0, htot-2),
}
}
func (ui *UI) renew() {
termbox.Flush()
wtot, htot := termbox.Size()
widths := getWidths(wtot)
wacc := 0
wlen := len(widths)
for i := 0; i < wlen; i++ {
ui.wins[i].renew(widths[i], htot-2, wacc, 1)
wacc += widths[i]
}
ui.msgwin.renew(wtot, 1, 0, htot-1)
}
func (ui *UI) echoFileInfo(nav *Nav) {
dir := nav.currDir()
if len(dir.fi) == 0 {
return
}
curr := nav.currFile()
ui.message = fmt.Sprintf("%v %v %v", curr.Mode(), humanize(curr.Size()), curr.ModTime().Format(time.ANSIC))
}
func (ui *UI) clearMsg() {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
win := ui.msgwin
win.printl(0, 0, fg, bg, "")
termbox.SetCursor(win.x, win.y)
termbox.Flush()
}
func (ui *UI) draw(nav *Nav) {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
termbox.Clear(fg, bg)
defer termbox.Flush()
dir := nav.currDir()
path := strings.Replace(dir.path, envHome, "~", -1)
ui.pwdwin.printf(0, 0, termbox.AttrBold|termbox.ColorGreen, bg, "%s@%s", envUser, envHost)
ui.pwdwin.printf(len(envUser)+len(envHost)+1, 0, fg, bg, ":")
ui.pwdwin.printf(len(envUser)+len(envHost)+2, 0, termbox.AttrBold|termbox.ColorBlue, bg, "%s", path)
length := min(len(ui.wins), len(nav.dirs))
woff := len(ui.wins) - length
if gOpts.preview {
length = min(len(ui.wins)-1, len(nav.dirs))
woff = len(ui.wins) - 1 - length
}
doff := len(nav.dirs) - length
for i := 0; i < length; i++ {
ui.wins[woff+i].printd(nav.dirs[doff+i], nav.marks)
}
defer ui.msgwin.print(0, 0, fg, bg, ui.message)
if gOpts.preview {
if len(dir.fi) == 0 {
return
}
preview := ui.wins[len(ui.wins)-1]
path := nav.currPath()
f, err := os.Stat(path)
if err != nil {
msg := fmt.Sprintf("getting file information: %s", err)
ui.message = msg
log.Print(msg)
return
}
if f.IsDir() {
dir := newDir(path)
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[path])
preview.printd(dir, nav.marks)
} else if f.Mode().IsRegular() {
file, err := os.Open(path)
if err != nil {
msg := fmt.Sprintf("opening file: %s", err)
ui.message = msg
log.Print(msg)
}
if err := preview.printr(file); err != nil {
ui.message = err.Error()
log.Print(err)
}
}
}
}
func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok bool) {
binds = make(map[string]Expr)
for key, expr := range keys {
if strings.HasPrefix(key, prefix) {
binds[key] = expr
if key == prefix {
ok = true
}
}
}
return
}
func (ui *UI) getExpr(nav *Nav) Expr {
r := &CallExpr{"renew", nil}
var acc []rune
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
if ev.Ch != 0 {
switch ev.Ch {
case '<':
acc = append(acc, '<', 'l', 't', '>')
case '>':
acc = append(acc, '<', 'g', 't', '>')
default:
acc = append(acc, ev.Ch)
}
} else {
switch ev.Key {
case termbox.KeyF1:
acc = append(acc, '<', 'f', '-', '1', '>')
case termbox.KeyF2:
acc = append(acc, '<', 'f', '-', '2', '>')
case termbox.KeyF3:
acc = append(acc, '<', 'f', '-', '3', '>')
case termbox.KeyF4:
acc = append(acc, '<', 'f', '-', '4', '>')
case termbox.KeyF5:
acc = append(acc, '<', 'f', '-', '5', '>')
case termbox.KeyF6:
acc = append(acc, '<', 'f', '-', '6', '>')
case termbox.KeyF7:
acc = append(acc, '<', 'f', '-', '7', '>')
case termbox.KeyF8:
acc = append(acc, '<', 'f', '-', '8', '>')
case termbox.KeyF9:
acc = append(acc, '<', 'f', '-', '9', '>')
case termbox.KeyF10:
acc = append(acc, '<', 'f', '-', '1', '0', '>')
case termbox.KeyF11:
acc = append(acc, '<', 'f', '-', '1', '1', '>')
case termbox.KeyF12:
acc = append(acc, '<', 'f', '-', '1', '2', '>')
case termbox.KeyInsert:
acc = append(acc, '<', 'i', 'n', 's', 'e', 'r', 't', '>')
case termbox.KeyDelete:
acc = append(acc, '<', 'd', 'e', 'l', 'e', 't', 'e', '>')
case termbox.KeyHome:
acc = append(acc, '<', 'h', 'o', 'm', 'e', '>')
case termbox.KeyEnd:
acc = append(acc, '<', 'e', 'n', 'd', '>')
case termbox.KeyPgup:
acc = append(acc, '<', 'p', 'g', 'u', 'p', '>')
case termbox.KeyPgdn:
acc = append(acc, '<', 'p', 'g', 'd', 'n', '>')
case termbox.KeyArrowUp:
acc = append(acc, '<', 'u', 'p', '>')
case termbox.KeyArrowDown:
acc = append(acc, '<', 'd', 'o', 'w', 'n', '>')
case termbox.KeyArrowLeft:
acc = append(acc, '<', 'l', 'e', 'f', 't', '>')
case termbox.KeyArrowRight:
acc = append(acc, '<', 'r', 'i', 'g', 'h', 't', '>')
case termbox.KeyCtrlSpace: // also KeyCtrlTilde and KeyCtrl2
acc = append(acc, '<', 'c', '-', 's', 'p', 'a', 'c', 'e', '>')
case termbox.KeyCtrlA:
acc = append(acc, '<', 'c', '-', 'a', '>')
case termbox.KeyCtrlB:
acc = append(acc, '<', 'c', '-', 'b', '>')
case termbox.KeyCtrlC:
acc = append(acc, '<', 'c', '-', 'c', '>')
case termbox.KeyCtrlD:
acc = append(acc, '<', 'c', '-', 'd', '>')
case termbox.KeyCtrlE:
acc = append(acc, '<', 'c', '-', 'e', '>')
case termbox.KeyCtrlF:
acc = append(acc, '<', 'c', '-', 'f', '>')
case termbox.KeyCtrlG:
acc = append(acc, '<', 'c', '-', 'g', '>')
case termbox.KeyBackspace: // also KeyCtrlH
acc = append(acc, '<', 'b', 's', '>')
case termbox.KeyTab: // also KeyCtrlI
acc = append(acc, '<', 't', 'a', 'b', '>')
case termbox.KeyCtrlJ:
acc = append(acc, '<', 'c', '-', 'j', '>')
case termbox.KeyCtrlK:
acc = append(acc, '<', 'c', '-', 'k', '>')
case termbox.KeyCtrlL:
acc = append(acc, '<', 'c', '-', 'l', '>')
case termbox.KeyEnter: // also KeyCtrlM
acc = append(acc, '<', 'e', 'n', 't', 'e', 'r', '>')
case termbox.KeyCtrlN:
acc = append(acc, '<', 'c', '-', 'n', '>')
case termbox.KeyCtrlO:
acc = append(acc, '<', 'c', '-', 'o', '>')
case termbox.KeyCtrlP:
acc = append(acc, '<', 'c', '-', 'p', '>')
case termbox.KeyCtrlQ:
acc = append(acc, '<', 'c', '-', 'q', '>')
case termbox.KeyCtrlR:
acc = append(acc, '<', 'c', '-', 'r', '>')
case termbox.KeyCtrlS:
acc = append(acc, '<', 'c', '-', 's', '>')
case termbox.KeyCtrlT:
acc = append(acc, '<', 'c', '-', 't', '>')
case termbox.KeyCtrlU:
acc = append(acc, '<', 'c', '-', 'u', '>')
case termbox.KeyCtrlV:
acc = append(acc, '<', 'c', '-', 'v', '>')
case termbox.KeyCtrlW:
acc = append(acc, '<', 'c', '-', 'w', '>')
case termbox.KeyCtrlX:
acc = append(acc, '<', 'c', '-', 'x', '>')
case termbox.KeyCtrlY:
acc = append(acc, '<', 'c', '-', 'y', '>')
case termbox.KeyCtrlZ:
acc = append(acc, '<', 'c', '-', 'z', '>')
case termbox.KeyEsc: // also KeyCtrlLsqBracket and KeyCtrl3
acc = nil
return r
case termbox.KeyCtrlBackslash: // also KeyCtrl4
acc = append(acc, '<', 'c', '-', '\\', '>')
case termbox.KeyCtrlRsqBracket: // also KeyCtrl5
acc = append(acc, '<', 'c', '-', ']', '>')
case termbox.KeyCtrl6:
acc = append(acc, '<', 'c', '-', '6', '>')
case termbox.KeyCtrlSlash: // also KeyCtrlUnderscore and KeyCtrl7
acc = append(acc, '<', 'c', '-', '/', '>')
case termbox.KeySpace:
acc = append(acc, '<', 's', 'p', 'a', 'c', 'e', '>')
case termbox.KeyBackspace2: // also KeyCtrl8
acc = append(acc, '<', 'b', 's', '2', '>')
}
}
binds, ok := findBinds(gOpts.keys, string(acc))
switch len(binds) {
case 0:
ui.message = fmt.Sprintf("unknown mapping: %s", string(acc))
acc = nil
return r
case 1:
if ok {
return gOpts.keys[string(acc)]
}
ui.draw(nav)
ui.listBinds(binds)
default:
if ok {
// TODO: use a delay
return gOpts.keys[string(acc)]
}
ui.draw(nav)
ui.listBinds(binds)
}
case termbox.EventResize:
return r
default:
// TODO: handle other events
}
}
}
func (ui *UI) prompt(nav *Nav, pref string) string {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
win := ui.msgwin
win.printl(0, 0, fg, bg, pref)
termbox.SetCursor(win.x+len(pref), win.y)
defer termbox.HideCursor()
termbox.Flush()
var acc []rune
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
if ev.Ch != 0 {
acc = append(acc, ev.Ch)
} else {
// TODO: rest of the keys
switch ev.Key {
case termbox.KeySpace:
acc = append(acc, ' ')
case termbox.KeyBackspace2:
if len(acc) > 0 {
acc = acc[:len(acc)-1]
}
case termbox.KeyEnter:
win.printl(0, 0, fg, bg, "")
termbox.SetCursor(win.x, win.y)
termbox.Flush()
return string(acc)
case termbox.KeyTab:
var matches []string
if pref == ":" {
matches, acc = compCmd(acc)
} else {
matches, acc = compShell(acc)
}
ui.draw(nav)
if len(matches) > 1 {
ui.listMatches(matches)
}
case termbox.KeyEsc:
return ""
}
}
win.printl(0, 0, fg, bg, pref)
win.print(len(pref), 0, fg, bg, string(acc))
termbox.SetCursor(win.x+len(pref)+len(acc), win.y)
termbox.Flush()
default:
// TODO: handle other events
}
}
}
func (ui *UI) pause() {
termbox.Close()
}
func (ui *UI) resume() {
if err := termbox.Init(); err != nil {
log.Fatalf("initializing termbox: %s", err)
}
}
func (ui *UI) sync() {
if err := termbox.Sync(); err != nil {
log.Printf("syncing termbox: %s", err)
}
}
func (ui *UI) showMenu(b *bytes.Buffer) {
lines := strings.Split(b.String(), "\n")
lines = lines[:len(lines)-1]
ui.menuwin.h = len(lines) - 1
ui.menuwin.y = ui.wins[0].h - ui.menuwin.h
ui.menuwin.printl(0, 0, termbox.AttrBold, termbox.AttrBold, lines[0])
for i, line := range lines[1:] {
ui.menuwin.printl(0, i+1, termbox.ColorDefault, termbox.ColorDefault, "")
ui.menuwin.print(0, i+1, termbox.ColorDefault, termbox.ColorDefault, line)
}
termbox.Flush()
}
func (ui *UI) listBinds(binds map[string]Expr) {
t := new(tabwriter.Writer)
b := new(bytes.Buffer)
var keys []string
for k := range binds {
keys = append(keys, k)
}
sort.Strings(keys)
t.Init(b, 0, gOpts.tabstop, 2, '\t', 0)
fmt.Fprintln(t, "keys\tcommand")
for _, k := range keys {
fmt.Fprintf(t, "%s\t%v\n", k, binds[k])
}
t.Flush()
ui.showMenu(b)
}
func (ui *UI) listMatches(matches []string) {
b := new(bytes.Buffer)
wtot, _ := termbox.Size()
wcol := 0
for _, m := range matches {
wcol = max(wcol, len(m))
}
wcol += gOpts.tabstop - wcol%gOpts.tabstop
ncol := wtot / wcol
b.WriteString("possible matches\n")
for i := 0; i < len(matches); i++ {
for j := 0; j < ncol && i < len(matches); i, j = i+1, j+1 {
b.WriteString(fmt.Sprintf("%s%*s", matches[i], wcol-len(matches[i]), ""))
}
b.WriteByte('\n')
}
ui.showMenu(b)
}