diff --git a/_examples/keybinds.go b/_examples/keybinds.go new file mode 100644 index 00000000..ccd900d0 --- /dev/null +++ b/_examples/keybinds.go @@ -0,0 +1,89 @@ +package main + +import ( + "log" + + "github.com/awesome-gocui/gocui" +) + +// layout generates the view +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + if v, err := g.SetView("hello", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + + v.Write([]byte("Hello")) + + if _, err := g.SetCurrentView("hello"); err != nil { + return err + } + } + + return nil +} + +// quit stops the gui +func quit(_ *gocui.Gui, _ *gocui.View) error { + return gocui.ErrQuit +} + +func main() { + // Create a gui + g, err := gocui.NewGui(gocui.OutputNormal, false) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + // Add a manager function + g.SetManagerFunc(layout) + + // This will set up the recovery for MustParse + defer func() { + if r := recover(); r != nil { + log.Panicln("Error caught: ", r) + } + }() + + // The MustParse can panic, but only returns 2 values instead of 3 + keyForced, modForced := gocui.MustParse("q") + if err := g.SetKeybinding("", keyForced, modForced, quit); err != nil { + log.Panicln(err) + } + + // We can blacklist a keybinding. + // This allows us to prevent setting the keybinding. + if err := g.BlacklistKeybinding(gocui.KeyCtrlC); err != nil { + log.Panic(err) + } + + // If for some reason you want to whitelist the keybinding, + // you can allow it again by calling g.WhitelistKeybinding. + if err := g.WhitelistKeybinding(gocui.KeyCtrlC); err != nil { + log.Panic(err) + } + + // The normal parse returns an key, a modifier and an error + keyNormal, modNormal, err := gocui.Parse("Ctrl+C") + if err != nil { + log.Panicln(err) + } + + if err = g.SetKeybinding("", keyNormal, modNormal, quit); err != nil { + log.Panicln(err) + } + + // You can still block it when it is set, just blacklist it again, this will not throw + // an error at parsing, since it is already parsed above, + // but it will prevent it from being executed + //if err := g.BlacklistKeybinding(gocui.KeyCtrlC); err != nil { + // log.Panicln(err) + //} + + // Now just start a mainloop for the demo + if err = g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} diff --git a/edit.go b/edit.go index b99f74f9..b5630df3 100644 --- a/edit.go +++ b/edit.go @@ -53,6 +53,14 @@ func simpleEditor(v *View, key Key, ch rune, mod Modifier) { v.MoveCursor(-1, 0, false) case key == KeyArrowRight: v.MoveCursor(1, 0, false) + case key == KeyTab: + v.EditWrite('\t') + case key == KeySpace: + v.EditWrite(' ') + case key == KeyInsert: + v.Overwrite = !v.Overwrite + default: + v.EditWrite(ch) } } @@ -63,6 +71,48 @@ func (v *View) EditWrite(ch rune) { v.moveCursor(w, 0, true) } +// EditDeleteToStartOfLine is the equivalent of pressing ctrl+U in your terminal, it deletes to the start of the line. Or if you are already at the start of the line, it deletes the newline character +func (v *View) EditDeleteToStartOfLine() { + x, _ := v.Cursor() + if x == 0 { + v.EditDelete(true) + } else { + // delete characters until we are the start of the line + for x > 0 { + v.EditDelete(true) + x, _ = v.Cursor() + } + } +} + +// EditGotoToStartOfLine takes you to the start of the current line +func (v *View) EditGotoToStartOfLine() { + x, _ := v.Cursor() + for x > 0 { + v.MoveCursor(-1, 0, false) + x, _ = v.Cursor() + } +} + +// EditGotoToEndOfLine takes you to the end of the line +func (v *View) EditGotoToEndOfLine() { + _, y := v.Cursor() + _ = v.SetCursor(0, y+1) + x, newY := v.Cursor() + if newY == y { + // we must be on the last line, so lets move to the very end + prevX := -1 + for prevX != x { + prevX = x + v.MoveCursor(1, 0, false) + x, _ = v.Cursor() + } + } else { + // most left so now we're at the end of the original line + v.MoveCursor(-1, 0, false) + } +} + // EditDelete deletes a rune at the cursor position. back determines the // direction. func (v *View) EditDelete(back bool) { diff --git a/gui.go b/gui.go index 73a0bd8c..6fe0d5d8 100644 --- a/gui.go +++ b/gui.go @@ -17,11 +17,23 @@ import ( type OutputMode termbox.OutputMode var ( - // ErrQuit is used to decide if the MainLoop finished successfully. - ErrQuit = standardErrors.New("quit") + // ErrAlreadyBlacklisted is returned when the keybinding is already blacklisted. + ErrAlreadyBlacklisted = standardErrors.New("keybind already blacklisted") + + // ErrBlacklisted is returned when the keybinding being parsed / used is blacklisted. + ErrBlacklisted = standardErrors.New("keybind blacklisted") + + // ErrNotBlacklisted is returned when a keybinding being whitelisted is not blacklisted. + ErrNotBlacklisted = standardErrors.New("keybind not blacklisted") + + // ErrNoSuchKeybind is returned when the keybinding being parsed does not exist. + ErrNoSuchKeybind = standardErrors.New("no such keybind") // ErrUnknownView allows to assert if a View must be initialized. ErrUnknownView = standardErrors.New("unknown view") + + // ErrQuit is used to decide if the MainLoop finished successfully. + ErrQuit = standardErrors.New("quit") ) const ( @@ -50,6 +62,7 @@ type Gui struct { maxX, maxY int outputMode OutputMode stop chan struct{} + blacklist []Key // BgColor and FgColor allow to configure the background and foreground // colors of the GUI. @@ -183,6 +196,17 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er return v, errors.Wrap(ErrUnknownView, 0) } +// SetViewBeneath sets a view stacked beneath another view +func (g *Gui) SetViewBeneath(name string, aboveViewName string, height int) (*View, error) { + aboveView, err := g.View(aboveViewName) + if err != nil { + return nil, err + } + + viewTop := aboveView.y1 + 1 + return g.SetView(name, aboveView.x0, viewTop, aboveView.x1, viewTop+height-1, 0) +} + // SetViewOnTop sets the given view on top of the existing ones. func (g *Gui) SetViewOnTop(name string) (*View, error) { for i, v := range g.views { @@ -285,6 +309,11 @@ func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, hand if err != nil { return err } + + if g.isBlacklisted(k) { + return ErrBlacklisted + } + kb = newKeybinding(viewname, k, ch, mod, handler) g.keybindings = append(g.keybindings, kb) return nil @@ -317,6 +346,28 @@ func (g *Gui) DeleteKeybindings(viewname string) { g.keybindings = s } +// BlackListKeybinding adds a keybinding to the blacklist +func (g *Gui) BlacklistKeybinding(k Key) error { + for _, j := range g.blacklist { + if j == k { + return ErrAlreadyBlacklisted + } + } + g.blacklist = append(g.blacklist, k) + return nil +} + +// WhiteListKeybinding removes a keybinding from the blacklist +func (g *Gui) WhitelistKeybinding(k Key) error { + for i, j := range g.blacklist { + if j == k { + g.blacklist = append(g.blacklist[:i], g.blacklist[i+1:]...) + return nil + } + } + return ErrNotBlacklisted +} + // getKey takes an empty interface with a key and returns the corresponding // typed Key or rune. func getKey(key interface{}) (Key, rune, error) { @@ -722,34 +773,54 @@ func (g *Gui) onKey(ev *termbox.Event) error { // and event. The value of matched is true if there is a match and no errors. func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) { var globalKb *keybinding + for _, kb := range g.keybindings { if kb.handler == nil { continue } + if !kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) { continue } + if kb.matchView(v) { return g.execKeybinding(v, kb) } + if kb.viewName == "" && ((v != nil && !v.Editable) || kb.ch == 0) { globalKb = kb } } + if globalKb != nil { return g.execKeybinding(v, globalKb) } + return false, nil } // execKeybinding executes a given keybinding func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) { + if g.isBlacklisted(kb.key) { + return true, nil + } + if err := kb.handler(g, v); err != nil { return false, err } return true, nil } +// isBlacklisted reports whether the key is blacklisted +func (g *Gui) isBlacklisted(k Key) bool { + for _, j := range g.blacklist { + if j == k { + return true + } + } + return false +} + // IsUnknownView reports whether the contents of an error is "unknown view". func IsUnknownView(err error) bool { return err != nil && err.Error() == ErrUnknownView.Error() diff --git a/keybinding.go b/keybinding.go index fb8aff3d..d294e70d 100644 --- a/keybinding.go +++ b/keybinding.go @@ -4,7 +4,18 @@ package gocui -import "github.com/awesome-gocui/termbox-go" +import ( + "strings" + + "github.com/awesome-gocui/termbox-go" +) + +// Key represents special keys or keys combinations. +type Key termbox.Key + +// Modifier allows to define special keys combinations. They can be used +// in combination with Keys or Runes when a new keybinding is defined. +type Modifier termbox.Modifier // Keybidings are used to link a given key-press event with a handler. type keybinding struct { @@ -15,6 +26,71 @@ type keybinding struct { handler func(*Gui, *View) error } +// Parse takes the input string and extracts the keybinding. +// Returns a Key / rune, a Modifier and an error. +func Parse(input string) (interface{}, Modifier, error) { + if len(input) == 1 { + _, r, err := getKey(rune(input[0])) + if err != nil { + return nil, ModNone, err + } + return r, ModNone, nil + } + + var modifier Modifier + cleaned := make([]string, 0) + + tokens := strings.Split(input, "+") + for _, t := range tokens { + normalized := strings.Title(strings.ToLower(t)) + if t == "Alt" { + modifier = ModAlt + continue + } + cleaned = append(cleaned, normalized) + } + + key, exist := translate[strings.Join(cleaned, "")] + if !exist { + return nil, ModNone, ErrNoSuchKeybind + } + + return key, modifier, nil +} + +// ParseAll takes an array of strings and returns a map of all keybindings. +func ParseAll(input []string) (map[interface{}]Modifier, error) { + ret := make(map[interface{}]Modifier) + for _, i := range input { + k, m, err := Parse(i) + if err != nil { + return ret, err + } + ret[k] = m + } + return ret, nil +} + +// MustParse takes the input string and returns a Key / rune and a Modifier. +// It will panic if any error occured. +func MustParse(input string) (interface{}, Modifier) { + k, m, err := Parse(input) + if err != nil { + panic(err) + } + return k, m +} + +// MustParseAll takes an array of strings and returns a map of all keybindings. +// It will panic if any error occured. +func MustParseAll(input []string) map[interface{}]Modifier { + result, err := ParseAll(input) + if err != nil { + panic(err) + } + return result +} + // newKeybinding returns a new Keybinding object. func newKeybinding(viewname string, key Key, ch rune, mod Modifier, handler func(*Gui, *View) error) (kb *keybinding) { kb = &keybinding{ @@ -41,8 +117,83 @@ func (kb *keybinding) matchView(v *View) bool { return kb.viewName == v.name } -// Key represents special keys or keys combinations. -type Key termbox.Key +// translations for strings to keys +var translate = map[string]Key{ + "F1": KeyF1, + "F2": KeyF2, + "F3": KeyF3, + "F4": KeyF4, + "F5": KeyF5, + "F6": KeyF6, + "F7": KeyF7, + "F8": KeyF8, + "F9": KeyF9, + "F10": KeyF10, + "F11": KeyF11, + "F12": KeyF12, + "Insert": KeyInsert, + "Delete": KeyDelete, + "Home": KeyHome, + "End": KeyEnd, + "Pgup": KeyPgup, + "Pgdn": KeyPgdn, + "ArrowUp": KeyArrowUp, + "ArrowDown": KeyArrowDown, + "ArrowLeft": KeyArrowLeft, + "ArrowRight": KeyArrowRight, + "CtrlTilde": KeyCtrlTilde, + "Ctrl2": KeyCtrl2, + "CtrlSpace": KeyCtrlSpace, + "CtrlA": KeyCtrlA, + "CtrlB": KeyCtrlB, + "CtrlC": KeyCtrlC, + "CtrlD": KeyCtrlD, + "CtrlE": KeyCtrlE, + "CtrlF": KeyCtrlF, + "CtrlG": KeyCtrlG, + "Backspace": KeyBackspace, + "CtrlH": KeyCtrlH, + "Tab": KeyTab, + "CtrlI": KeyCtrlI, + "CtrlJ": KeyCtrlJ, + "CtrlK": KeyCtrlK, + "CtrlL": KeyCtrlL, + "Enter": KeyEnter, + "CtrlM": KeyCtrlM, + "CtrlN": KeyCtrlN, + "CtrlO": KeyCtrlO, + "CtrlP": KeyCtrlP, + "CtrlQ": KeyCtrlQ, + "CtrlR": KeyCtrlR, + "CtrlS": KeyCtrlS, + "CtrlT": KeyCtrlT, + "CtrlU": KeyCtrlU, + "CtrlV": KeyCtrlV, + "CtrlW": KeyCtrlW, + "CtrlX": KeyCtrlX, + "CtrlY": KeyCtrlY, + "CtrlZ": KeyCtrlZ, + "Esc": KeyEsc, + "CtrlLsqBracket": KeyCtrlLsqBracket, + "Ctrl3": KeyCtrl3, + "Ctrl4": KeyCtrl4, + "CtrlBackslash": KeyCtrlBackslash, + "Ctrl5": KeyCtrl5, + "CtrlRsqBracket": KeyCtrlRsqBracket, + "Ctrl6": KeyCtrl6, + "Ctrl7": KeyCtrl7, + "CtrlSlash": KeyCtrlSlash, + "CtrlUnderscore": KeyCtrlUnderscore, + "Space": KeySpace, + "Backspace2": KeyBackspace2, + "Ctrl8": KeyCtrl8, + "Mouseleft": MouseLeft, + "Mousemiddle": MouseMiddle, + "Mouseright": MouseRight, + "Mouserelease": MouseRelease, + "MousewheelUp": MouseWheelUp, + "MousewheelDown": MouseWheelDown, +} // Special keys. const ( @@ -127,10 +278,6 @@ const ( KeyCtrl8 = Key(termbox.KeyCtrl8) ) -// Modifier allows to define special keys combinations. They can be used -// in combination with Keys or Runes when a new keybinding is defined. -type Modifier termbox.Modifier - // Modifiers. const ( ModNone Modifier = Modifier(0) diff --git a/view.go b/view.go index 9c7eb3eb..81f90603 100644 --- a/view.go +++ b/view.go @@ -470,6 +470,11 @@ func (v *View) Rewind() { } } +// IsTainted tells us if the view is tainted +func (v *View) IsTainted() bool { + return v.tainted +} + // draw re-draws the view's contents. func (v *View) draw() error { if !v.Visible { @@ -635,10 +640,16 @@ func (v *View) ViewBufferLines() []string { return lines } +// LinesHeight is the count of view lines (i.e. lines excluding wrapping) func (v *View) LinesHeight() int { return len(v.lines) } +// ViewLinesHeight is the count of view lines (i.e. lines including wrapping) +func (v *View) ViewLinesHeight() int { + return len(v.viewLines) +} + // ViewBuffer returns a string with the contents of the view's buffer that is // shown to the user. func (v *View) ViewBuffer() string {