lf/ui.go
KenjiTakahashi 8d4a3192f1 Pass width and height to the previewer
This way, the script can do a little magic like

 head -n $2 $1 | highlight -O ansi --syntax ${1##*.}

to avoid reading whole (possibly huge) file.
2016-09-01 00:19:08 +02:00

745 lines
17 KiB
Go

package main
import (
"bufio"
"bytes"
"fmt"
"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 {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
buf := bufio.NewScanner(reg)
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())
}
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)
}
buf = bufio.NewScanner(bytes.NewReader(out))
} else {
reg.Seek(0, 0)
buf = bufio.NewScanner(reg)
}
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)
}