From 5f87cb25420b44cfec79d93e86c8eb71107ffafc Mon Sep 17 00:00:00 2001 From: Gokcehan Date: Thu, 15 Dec 2016 12:26:06 +0300 Subject: [PATCH] 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. --- app.go | 140 +-------------------- eval.go | 166 ++++++++++++++++++------- ui.go | 378 +++++++++++++++++++++++++++++++++----------------------- 3 files changed, 343 insertions(+), 341 deletions(-) diff --git a/app.go b/app.go index 5a62540..a363df2 100644 --- a/app.go +++ b/app.go @@ -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) == "" { - 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 diff --git a/eval.go b/eval.go index c78c573..f31343f 100644 --- a/eval.go +++ b/eval.go @@ -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) } } diff --git a/ui.go b/ui.go index 0709db7..520be7d 100644 --- a/ui.go +++ b/ui.go @@ -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 "": ev.Ch = '<' case "": 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 acc []rune + var cnt []rune - var lacc []rune - var racc []rune + go func() { + for { + ev := ui.pollEvent() - var buf []rune - - for { - switch ev := ui.pollEvent(); ev.Type { - case termbox.EventKey: - if ev.Ch != 0 { - lacc = append(lacc, ev.Ch) - } else { - // TODO: rest of the keys - switch ev.Key { - case termbox.KeyEsc: - return "" - case termbox.KeySpace: - lacc = append(lacc, ' ') - case termbox.KeyTab: - var matches []string - if pref == ":" { - matches, lacc = compCmd(lacc) + if ui.cmdpref != "" { + switch ev.Type { + case termbox.EventKey: + if ev.Ch != 0 { + ch <- MultiExpr{&CallExpr{"cmd-insert", []string{string(ev.Ch)}}, 1} } else { - matches, lacc = compShell(lacc) - } - ui.draw(nav) - if len(matches) > 1 { - ui.listMatches(matches) - } - case termbox.KeyEnter, termbox.KeyCtrlJ: - win.printl(0, 0, fg, bg, "") - termbox.Flush() - return string(append(lacc, racc...)) - case termbox.KeyBackspace, termbox.KeyBackspace2: - if len(lacc) > 0 { - lacc = lacc[:len(lacc)-1] - } - case termbox.KeyDelete, termbox.KeyCtrlD: - if len(racc) > 0 { - racc = racc[1:] - } - case termbox.KeyArrowLeft, termbox.KeyCtrlB: - if len(lacc) > 0 { - racc = append([]rune{lacc[len(lacc)-1]}, racc...) - lacc = lacc[:len(lacc)-1] - } - case termbox.KeyArrowRight, termbox.KeyCtrlF: - if len(racc) > 0 { - lacc = append(lacc, racc[0]) - racc = racc[1:] - } - case termbox.KeyHome, termbox.KeyCtrlA: - racc = append(lacc, racc...) - lacc = nil - case termbox.KeyEnd, termbox.KeyCtrlE: - lacc = append(lacc, racc...) - racc = nil - case termbox.KeyCtrlK: - if len(racc) > 0 { - buf = racc - racc = nil - } - case termbox.KeyCtrlU: - if len(lacc) > 0 { - buf = lacc - lacc = nil - } - case termbox.KeyCtrlW: - ind := strings.LastIndex(strings.TrimRight(string(lacc), " "), " ") + 1 - buf = lacc[ind:] - lacc = lacc[:ind] - case termbox.KeyCtrlY: - lacc = append(lacc, buf...) - case termbox.KeyCtrlT: - if len(lacc) > 1 { - lacc[len(lacc)-1], lacc[len(lacc)-2] = lacc[len(lacc)-2], lacc[len(lacc)-1] + // TODO: rest of the keys + switch ev.Key { + case termbox.KeyEsc: + ch <- MultiExpr{&CallExpr{"cmd-escape", nil}, 1} + case termbox.KeySpace: + ch <- MultiExpr{&CallExpr{"cmd-insert", []string{" "}}, 1} + case termbox.KeyTab: + ch <- MultiExpr{&CallExpr{"cmd-comp", nil}, 1} + case termbox.KeyEnter, termbox.KeyCtrlJ: + ch <- MultiExpr{&CallExpr{"cmd-enter", nil}, 1} + case termbox.KeyBackspace, termbox.KeyBackspace2: + ch <- MultiExpr{&CallExpr{"cmd-delete-back", nil}, 1} + case termbox.KeyDelete, termbox.KeyCtrlD: + ch <- MultiExpr{&CallExpr{"cmd-delete", nil}, 1} + case termbox.KeyArrowLeft, termbox.KeyCtrlB: + ch <- MultiExpr{&CallExpr{"cmd-left", nil}, 1} + case termbox.KeyArrowRight, termbox.KeyCtrlF: + ch <- MultiExpr{&CallExpr{"cmd-right", nil}, 1} + case termbox.KeyHome, termbox.KeyCtrlA: + ch <- MultiExpr{&CallExpr{"cmd-beg", nil}, 1} + case termbox.KeyEnd, termbox.KeyCtrlE: + ch <- MultiExpr{&CallExpr{"cmd-end", nil}, 1} + case termbox.KeyCtrlK: + ch <- MultiExpr{&CallExpr{"cmd-delete-end", nil}, 1} + case termbox.KeyCtrlU: + ch <- MultiExpr{&CallExpr{"cmd-delete-beg", nil}, 1} + case termbox.KeyCtrlW: + ch <- MultiExpr{&CallExpr{"cmd-delete-word", nil}, 1} + case termbox.KeyCtrlY: + ch <- MultiExpr{&CallExpr{"cmd-put", nil}, 1} + case termbox.KeyCtrlT: + 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() - default: - // TODO: handle other events + 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) == "" { + 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 }