implement asynchronous read commands

This commit changes previous reading command implementation to an
asynchronous implementation. By the nature of this change, this commit
touches many places in the ui and evaluator. Aim is to fix the following
problems:

- There is no race condition anymore when reading commands and other
commands update the ui at the same time.

- Autocompletion and keymenu is now drawn in the main draw event. This
should fix some ui glitches when a new menu is smaller than the previous
one.

- Window resize event when reading a command is now properly handled.

- Readline actions are now regular commands. This should make it
possible to change the default keybindings for these actions in the
future.

Mentioned in #36.
This commit is contained in:
Gokcehan 2016-12-15 12:26:06 +03:00
parent f66a4a4a2e
commit 5f87cb2542
3 changed files with 343 additions and 341 deletions

140
app.go
View File

@ -8,9 +8,6 @@ import (
"os/exec"
"strconv"
"strings"
"unicode"
"github.com/nsf/termbox-go"
)
type App struct {
@ -55,144 +52,11 @@ func waitKey() error {
return nil
}
// This function is used to read expressions on the client side. Prompting
// commands (e.g. "read") are recognized and evaluated while being read here.
// Digits are interpreted as command counts but this is only done for digits
// preceding any non-digit characters (e.g. "42y2k" as 42 times "y2k").
func (app *App) readExpr() chan MultiExpr {
ch := make(chan MultiExpr)
renew := &CallExpr{"renew", nil}
count := 1
var acc []rune
var cnt []rune
go func() {
for {
switch ev := app.ui.pollEvent(); ev.Type {
case termbox.EventKey:
if ev.Ch != 0 {
switch {
case ev.Ch == '<':
acc = append(acc, '<', 'l', 't', '>')
case ev.Ch == '>':
acc = append(acc, '<', 'g', 't', '>')
case unicode.IsDigit(ev.Ch) && len(acc) == 0:
cnt = append(cnt, ev.Ch)
default:
acc = append(acc, ev.Ch)
}
} else {
val := gKeyVal[ev.Key]
if string(val) == "<esc>" {
ch <- MultiExpr{renew, 1}
acc = nil
cnt = nil
}
acc = append(acc, val...)
}
binds, ok := findBinds(gOpts.keys, string(acc))
switch len(binds) {
case 0:
app.ui.message = fmt.Sprintf("unknown mapping: %s", string(acc))
ch <- MultiExpr{renew, 1}
acc = nil
cnt = nil
case 1:
if ok {
if len(cnt) > 0 {
c, err := strconv.Atoi(string(cnt))
if err != nil {
log.Printf("converting command count: %s", err)
}
count = c
} else {
count = 1
}
expr := gOpts.keys[string(acc)]
switch expr.(type) {
case *CallExpr:
switch expr.(*CallExpr).name {
case "read",
"read-shell",
"read-shell-wait",
"read-shell-async",
"search",
"search-back",
"push":
expr.eval(app, nil)
app.ui.loadFile(app.nav)
app.ui.draw(app.nav)
default:
ch <- MultiExpr{expr, count}
}
default:
ch <- MultiExpr{expr, count}
}
acc = nil
cnt = nil
}
if len(acc) > 0 {
app.ui.listBinds(binds)
}
default:
if ok {
// TODO: use a delay
if len(cnt) > 0 {
c, err := strconv.Atoi(string(cnt))
if err != nil {
log.Printf("converting command count: %s", err)
}
count = c
} else {
count = 1
}
expr := gOpts.keys[string(acc)]
switch expr.(type) {
case *CallExpr:
switch expr.(*CallExpr).name {
case "read",
"read-shell",
"read-shell-wait",
"read-shell-async",
"search",
"search-back",
"push":
expr.eval(app, nil)
app.ui.loadFile(app.nav)
app.ui.draw(app.nav)
default:
ch <- MultiExpr{expr, count}
}
default:
ch <- MultiExpr{expr, count}
}
acc = nil
cnt = nil
}
if len(acc) > 0 {
app.ui.listBinds(binds)
}
}
case termbox.EventResize:
ch <- MultiExpr{renew, 1}
default:
// TODO: handle other events
}
}
}()
return ch
}
// This is the main event loop of the application. There are two channels to
// read expressions from client and server. Reading and evaluation are done on
// different goroutines except for prompting commands (e.g. "read").
// separate goroutines.
func (app *App) handleInp() {
clientChan := app.readExpr()
clientChan := app.ui.readExpr()
var serverChan chan Expr

166
eval.go
View File

@ -242,56 +242,17 @@ func (e *CallExpr) eval(app *App, args []string) {
app.ui.loadFile(app.nav)
app.ui.loadFileInfo(app.nav)
case "read":
s := app.ui.prompt(app.nav, ":")
if len(s) == 0 {
return
}
log.Printf("command: %s", s)
p := newParser(strings.NewReader(s))
for p.parse() {
p.expr.eval(app, nil)
}
if p.err != nil {
app.ui.message = p.err.Error()
log.Print(p.err)
}
app.ui.cmdpref = ":"
case "read-shell":
s := app.ui.prompt(app.nav, "$")
if len(s) == 0 {
return
}
log.Printf("shell: %s", s)
app.runShell(s, nil, false, false)
app.ui.cmdpref = "$"
case "read-shell-wait":
s := app.ui.prompt(app.nav, "!")
if len(s) == 0 {
return
}
log.Printf("shell-wait: %s", s)
app.runShell(s, nil, true, false)
app.ui.cmdpref = "!"
case "read-shell-async":
s := app.ui.prompt(app.nav, "&")
if len(s) == 0 {
return
}
log.Printf("shell-async: %s", s)
app.runShell(s, nil, false, true)
app.ui.cmdpref = "&"
case "search":
s := app.ui.prompt(app.nav, "/")
if len(s) == 0 {
return
}
log.Printf("search: %s", s)
app.ui.message = "sorry, search is not implemented yet!"
// TODO: implement
app.ui.cmdpref = "/"
case "search-back":
s := app.ui.prompt(app.nav, "?")
if len(s) == 0 {
return
}
log.Printf("search-back: %s", s)
app.ui.message = "sorry, search-back is not implemented yet!"
// TODO: implement
app.ui.cmdpref = "?"
case "toggle":
app.nav.toggle()
case "invert":
@ -359,7 +320,118 @@ func (e *CallExpr) eval(app *App, args []string) {
app.ui.loadFileInfo(app.nav)
case "push":
if len(e.args) > 0 {
app.ui.keysbuf = append(app.ui.keysbuf, splitKeys(e.args[0])...)
log.Println("pushing keys", e.args[0])
for _, key := range splitKeys(e.args[0]) {
app.ui.keychan <- key
}
}
case "cmd-insert":
if len(e.args) > 0 {
app.ui.cmdlacc = append(app.ui.cmdlacc, []rune(e.args[0])...)
}
case "cmd-escape":
app.ui.menubuf = nil
app.ui.cmdbuf = nil
app.ui.cmdlacc = nil
app.ui.cmdracc = nil
app.ui.cmdpref = ""
case "cmd-comp":
var matches []string
if app.ui.cmdpref == ":" {
matches, app.ui.cmdlacc = compCmd(app.ui.cmdlacc)
} else {
matches, app.ui.cmdlacc = compShell(app.ui.cmdlacc)
}
app.ui.draw(app.nav)
if len(matches) > 1 {
app.ui.menubuf = listMatches(matches)
} else {
app.ui.menubuf = nil
}
case "cmd-enter":
s := string(append(app.ui.cmdlacc, app.ui.cmdracc...))
if len(s) == 0 {
return
}
app.ui.menubuf = nil
app.ui.cmdbuf = nil
app.ui.cmdlacc = nil
app.ui.cmdracc = nil
switch app.ui.cmdpref {
case ":":
log.Printf("command: %s", s)
p := newParser(strings.NewReader(s))
for p.parse() {
p.expr.eval(app, nil)
}
if p.err != nil {
app.ui.message = p.err.Error()
log.Print(p.err)
}
case "$":
log.Printf("shell: %s", s)
app.runShell(s, nil, false, false)
case "!":
log.Printf("shell-wait: %s", s)
app.runShell(s, nil, true, false)
case "&":
log.Printf("shell-async: %s", s)
app.runShell(s, nil, false, true)
case "/":
log.Printf("search: %s", s)
app.ui.message = "sorry, search is not implemented yet!"
// TODO: implement
case "?":
log.Printf("search-back: %s", s)
app.ui.message = "sorry, search-back is not implemented yet!"
// TODO: implement
default:
log.Printf("entering unknown execution prefix: %q", app.ui.cmdpref)
}
app.ui.cmdpref = ""
case "cmd-delete-back":
if len(app.ui.cmdlacc) > 0 {
app.ui.cmdlacc = app.ui.cmdlacc[:len(app.ui.cmdlacc)-1]
}
case "cmd-delete":
if len(app.ui.cmdracc) > 0 {
app.ui.cmdracc = app.ui.cmdracc[1:]
}
case "cmd-left":
if len(app.ui.cmdlacc) > 0 {
app.ui.cmdracc = append([]rune{app.ui.cmdlacc[len(app.ui.cmdlacc)-1]}, app.ui.cmdracc...)
app.ui.cmdlacc = app.ui.cmdlacc[:len(app.ui.cmdlacc)-1]
}
case "cmd-right":
if len(app.ui.cmdracc) > 0 {
app.ui.cmdlacc = append(app.ui.cmdlacc, app.ui.cmdracc[0])
app.ui.cmdracc = app.ui.cmdracc[1:]
}
case "cmd-beg":
app.ui.cmdracc = append(app.ui.cmdlacc, app.ui.cmdracc...)
app.ui.cmdlacc = nil
case "cmd-end":
app.ui.cmdlacc = append(app.ui.cmdlacc, app.ui.cmdracc...)
app.ui.cmdracc = nil
case "cmd-delete-end":
if len(app.ui.cmdracc) > 0 {
app.ui.cmdbuf = app.ui.cmdracc
app.ui.cmdracc = nil
}
case "cmd-delete-beg":
if len(app.ui.cmdlacc) > 0 {
app.ui.cmdbuf = app.ui.cmdlacc
app.ui.cmdlacc = nil
}
case "cmd-delete-word":
ind := strings.LastIndex(strings.TrimRight(string(app.ui.cmdlacc), " "), " ") + 1
app.ui.cmdbuf = app.ui.cmdlacc[ind:]
app.ui.cmdlacc = app.ui.cmdlacc[:ind]
case "cmd-put":
app.ui.cmdlacc = append(app.ui.cmdlacc, app.ui.cmdbuf...)
case "cmd-transpose":
if len(app.ui.cmdlacc) > 1 {
app.ui.cmdlacc[len(app.ui.cmdlacc)-1], app.ui.cmdlacc[len(app.ui.cmdlacc)-2] = app.ui.cmdlacc[len(app.ui.cmdlacc)-2], app.ui.cmdlacc[len(app.ui.cmdlacc)-1]
}
default:
cmd, ok := gOpts.cmds[e.name]
@ -391,7 +463,7 @@ func (e *ExecExpr) eval(app *App, args []string) {
log.Printf("search-back: %s -- %s", e, args)
// TODO: implement
default:
log.Printf("unknown execution prefix: %q", e.pref)
log.Printf("evaluating unknown execution prefix: %q", e.pref)
}
}

334
ui.go
View File

@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"text/tabwriter"
"unicode"
"unicode/utf8"
"github.com/nsf/termbox-go"
@ -340,7 +341,13 @@ type UI struct {
message string
regprev []string
dirprev *Dir
keysbuf []string
keychan chan string
evschan chan termbox.Event
cmdpref string
cmdlacc []rune
cmdracc []rune
cmdbuf []rune
menubuf *bytes.Buffer
}
func getWidths(wtot int) []int {
@ -376,11 +383,22 @@ func newUI() *UI {
wacc += widths[i]
}
key := make(chan string, 1000)
evs := make(chan termbox.Event)
go func() {
for {
evs <- termbox.PollEvent()
}
}()
return &UI{
wins: wins,
pwdwin: newWin(wtot, 1, 0, 0),
msgwin: newWin(wtot, 1, 0, htot-1),
menuwin: newWin(wtot, 1, 0, htot-2),
keychan: key,
evschan: evs,
}
}
@ -478,26 +496,11 @@ func (ui *UI) loadFile(nav *Nav) {
}
}
func (ui *UI) clearMsg() {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
win := ui.msgwin
win.printl(0, 0, fg, bg, "")
termbox.Flush()
}
func (ui *UI) draw(nav *Nav) {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
termbox.Clear(fg, bg)
// leave the cursor at the beginning of the current file for screen readers
var length, woff, doff int
defer func() {
fmt.Printf("[%d;%dH", ui.wins[woff+length-1].y+nav.dirs[doff+length-1].pos+1, ui.wins[woff+length-1].x+1)
}()
defer termbox.Flush()
dir := nav.currDir()
path := strings.Replace(dir.path, envHome, "~", -1)
@ -507,20 +510,28 @@ func (ui *UI) draw(nav *Nav) {
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
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
doff := len(nav.dirs) - length
for i := 0; i < length; i++ {
ui.wins[woff+i].printd(nav.dirs[doff+i], nav.marks, nav.saves)
}
defer ui.msgwin.print(0, 0, fg, bg, ui.message)
if ui.cmdpref != "" {
ui.msgwin.printl(0, 0, fg, bg, ui.cmdpref)
ui.msgwin.print(len(ui.cmdpref), 0, fg, bg, string(ui.cmdlacc))
ui.msgwin.print(len(ui.cmdpref)+runeSliceWidth(ui.cmdlacc), 0, fg, bg, string(ui.cmdracc))
termbox.SetCursor(ui.msgwin.x+len(ui.cmdpref)+runeSliceWidth(ui.cmdlacc), ui.msgwin.y)
} else {
ui.msgwin.print(0, 0, fg, bg, ui.message)
termbox.HideCursor()
}
if gOpts.preview {
f, err := nav.currFile()
@ -536,6 +547,28 @@ func (ui *UI) draw(nav *Nav) {
preview.printr(ui.regprev)
}
}
if ui.menubuf != nil {
lines := strings.Split(ui.menubuf.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()
if ui.cmdpref == "" {
// leave the cursor at the beginning of the current file for screen readers
fmt.Printf("[%d;%dH", ui.wins[woff+length-1].y+nav.dirs[doff+length-1].pos+1, ui.wins[woff+length-1].x+1)
}
}
func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok bool) {
@ -551,33 +584,55 @@ func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok b
return
}
func listBinds(binds map[string]Expr) *bytes.Buffer {
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()
return b
}
func (ui *UI) pollEvent() termbox.Event {
if len(ui.keysbuf) > 0 {
select {
case key := <-ui.keychan:
ev := termbox.Event{Type: termbox.EventKey}
keys := ui.keysbuf[0]
if len(keys) == 1 {
ev.Ch, _ = utf8.DecodeRuneInString(keys)
if len(key) == 1 {
ev.Ch, _ = utf8.DecodeRuneInString(key)
} else {
switch keys {
switch key {
case "<lt>":
ev.Ch = '<'
case "<gt>":
ev.Ch = '>'
default:
if val, ok := gValKey[keys]; ok {
if val, ok := gValKey[key]; ok {
ev.Key = val
} else {
ev.Key = termbox.KeyEsc
msg := fmt.Sprintf("unknown key: %s", keys)
msg := fmt.Sprintf("unknown key: %s", key)
ui.message = msg
log.Print(msg)
}
}
}
ui.keysbuf = ui.keysbuf[1:]
return ev
case ev := <-ui.evschan:
return ev
}
return termbox.PollEvent()
}
type MultiExpr struct {
@ -585,104 +640,152 @@ type MultiExpr struct {
count int
}
func (ui *UI) prompt(nav *Nav, pref string) string {
fg, bg := termbox.ColorDefault, termbox.ColorDefault
// This function is used to read expressions on the client side. Digits are
// interpreted as command counts but this is only done for digits preceding any
// non-digit characters (e.g. "42y2k" as 42 times "y2k").
func (ui *UI) readExpr() chan MultiExpr {
ch := make(chan MultiExpr)
win := ui.msgwin
renew := &CallExpr{"renew", nil}
count := 1
win.printl(0, 0, fg, bg, pref)
termbox.SetCursor(win.x+len(pref), win.y)
defer termbox.HideCursor()
termbox.Flush()
var lacc []rune
var racc []rune
var buf []rune
var acc []rune
var cnt []rune
go func() {
for {
switch ev := ui.pollEvent(); ev.Type {
ev := ui.pollEvent()
if ui.cmdpref != "" {
switch ev.Type {
case termbox.EventKey:
if ev.Ch != 0 {
lacc = append(lacc, ev.Ch)
ch <- MultiExpr{&CallExpr{"cmd-insert", []string{string(ev.Ch)}}, 1}
} else {
// TODO: rest of the keys
switch ev.Key {
case termbox.KeyEsc:
return ""
ch <- MultiExpr{&CallExpr{"cmd-escape", nil}, 1}
case termbox.KeySpace:
lacc = append(lacc, ' ')
ch <- MultiExpr{&CallExpr{"cmd-insert", []string{" "}}, 1}
case termbox.KeyTab:
var matches []string
if pref == ":" {
matches, lacc = compCmd(lacc)
} else {
matches, lacc = compShell(lacc)
}
ui.draw(nav)
if len(matches) > 1 {
ui.listMatches(matches)
}
ch <- MultiExpr{&CallExpr{"cmd-comp", nil}, 1}
case termbox.KeyEnter, termbox.KeyCtrlJ:
win.printl(0, 0, fg, bg, "")
termbox.Flush()
return string(append(lacc, racc...))
ch <- MultiExpr{&CallExpr{"cmd-enter", nil}, 1}
case termbox.KeyBackspace, termbox.KeyBackspace2:
if len(lacc) > 0 {
lacc = lacc[:len(lacc)-1]
}
ch <- MultiExpr{&CallExpr{"cmd-delete-back", nil}, 1}
case termbox.KeyDelete, termbox.KeyCtrlD:
if len(racc) > 0 {
racc = racc[1:]
}
ch <- MultiExpr{&CallExpr{"cmd-delete", nil}, 1}
case termbox.KeyArrowLeft, termbox.KeyCtrlB:
if len(lacc) > 0 {
racc = append([]rune{lacc[len(lacc)-1]}, racc...)
lacc = lacc[:len(lacc)-1]
}
ch <- MultiExpr{&CallExpr{"cmd-left", nil}, 1}
case termbox.KeyArrowRight, termbox.KeyCtrlF:
if len(racc) > 0 {
lacc = append(lacc, racc[0])
racc = racc[1:]
}
ch <- MultiExpr{&CallExpr{"cmd-right", nil}, 1}
case termbox.KeyHome, termbox.KeyCtrlA:
racc = append(lacc, racc...)
lacc = nil
ch <- MultiExpr{&CallExpr{"cmd-beg", nil}, 1}
case termbox.KeyEnd, termbox.KeyCtrlE:
lacc = append(lacc, racc...)
racc = nil
ch <- MultiExpr{&CallExpr{"cmd-end", nil}, 1}
case termbox.KeyCtrlK:
if len(racc) > 0 {
buf = racc
racc = nil
}
ch <- MultiExpr{&CallExpr{"cmd-delete-end", nil}, 1}
case termbox.KeyCtrlU:
if len(lacc) > 0 {
buf = lacc
lacc = nil
}
ch <- MultiExpr{&CallExpr{"cmd-delete-beg", nil}, 1}
case termbox.KeyCtrlW:
ind := strings.LastIndex(strings.TrimRight(string(lacc), " "), " ") + 1
buf = lacc[ind:]
lacc = lacc[:ind]
ch <- MultiExpr{&CallExpr{"cmd-delete-word", nil}, 1}
case termbox.KeyCtrlY:
lacc = append(lacc, buf...)
ch <- MultiExpr{&CallExpr{"cmd-put", nil}, 1}
case termbox.KeyCtrlT:
if len(lacc) > 1 {
lacc[len(lacc)-1], lacc[len(lacc)-2] = lacc[len(lacc)-2], lacc[len(lacc)-1]
ch <- MultiExpr{&CallExpr{"cmd-transpose", nil}, 1}
}
}
continue
}
}
win.printl(0, 0, fg, bg, pref)
win.print(len(pref), 0, fg, bg, string(lacc))
win.print(len(pref)+runeSliceWidth(lacc), 0, fg, bg, string(racc))
termbox.SetCursor(win.x+len(pref)+runeSliceWidth(lacc), win.y)
termbox.Flush()
switch ev.Type {
case termbox.EventKey:
if ev.Ch != 0 {
switch {
case ev.Ch == '<':
acc = append(acc, '<', 'l', 't', '>')
case ev.Ch == '>':
acc = append(acc, '<', 'g', 't', '>')
case unicode.IsDigit(ev.Ch) && len(acc) == 0:
cnt = append(cnt, ev.Ch)
default:
acc = append(acc, ev.Ch)
}
} else {
val := gKeyVal[ev.Key]
if string(val) == "<esc>" {
ch <- MultiExpr{renew, 1}
acc = nil
cnt = nil
}
acc = append(acc, val...)
}
binds, ok := findBinds(gOpts.keys, string(acc))
switch len(binds) {
case 0:
ui.message = fmt.Sprintf("unknown mapping: %s", string(acc))
ch <- MultiExpr{renew, 1}
acc = nil
cnt = nil
case 1:
if ok {
if len(cnt) > 0 {
c, err := strconv.Atoi(string(cnt))
if err != nil {
log.Printf("converting command count: %s", err)
}
count = c
} else {
count = 1
}
expr := gOpts.keys[string(acc)]
ch <- MultiExpr{expr, count}
acc = nil
cnt = nil
}
if len(acc) > 0 {
ui.menubuf = listBinds(binds)
ch <- MultiExpr{renew, 1}
} else if ui.menubuf != nil {
ui.menubuf = nil
}
default:
if ok {
// TODO: use a delay
if len(cnt) > 0 {
c, err := strconv.Atoi(string(cnt))
if err != nil {
log.Printf("converting command count: %s", err)
}
count = c
} else {
count = 1
}
expr := gOpts.keys[string(acc)]
ch <- MultiExpr{expr, count}
acc = nil
cnt = nil
}
if len(acc) > 0 {
ui.menubuf = listBinds(binds)
ch <- MultiExpr{renew, 1}
} else {
ui.menubuf = nil
}
}
case termbox.EventResize:
ch <- MultiExpr{renew, 1}
default:
// TODO: handle other events
}
}
}()
return ch
}
func (ui *UI) pause() {
@ -701,44 +804,7 @@ func (ui *UI) sync() {
}
}
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) {
func listMatches(matches []string) *bytes.Buffer {
b := new(bytes.Buffer)
wtot, _ := termbox.Size()
@ -759,5 +825,5 @@ func (ui *UI) listMatches(matches []string) {
b.WriteByte('\n')
}
ui.showMenu(b)
return b
}