From da1361c0a7a586792734c928489b4953b74465f6 Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Mon, 9 Sep 2019 19:29:20 +0200 Subject: [PATCH 1/6] View: Added ViewLinesHeight() * This might be usefull for people who want to check that See https://github.com/jesseduffield/gocui/commit/e030e03faca4edb742120c0b739cb2114a4b3358 Co-authored-by: Jesse Duffield Signed-off-by: Glenn Vriesman --- view.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/view.go b/view.go index 9c7eb3eb..9257e010 100644 --- a/view.go +++ b/view.go @@ -635,10 +635,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 { From 8c371336b50d299f0bdebf1e62b16c435a62ebc4 Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Mon, 9 Sep 2019 19:32:55 +0200 Subject: [PATCH 2/6] View: Added IsTained() * This might be usefull for people who want to check that See https://github.com/jesseduffield/gocui/commit/77d76180000321e6c543d0879c36016a15537682 Co-authored-by: Jesse Duffield Signed-off-by: Glenn Vriesman --- view.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/view.go b/view.go index 9257e010..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 { From 6dc74ecc12d0b61f0d940777ed4644bb31d7de25 Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Mon, 9 Sep 2019 19:38:37 +0200 Subject: [PATCH 3/6] Edit: Added editor functions * Added a handler for tab * Added a handler for space * Added a handler for Insert * Added a handler for Ctrl+U * Added a handler for Ctrl+A * Added a handler for Ctrl+E * Defaults to EditWrite() See https://github.com/jesseduffield/gocui/commit/77d76180000321e6c543d0879c36016a15537682 Co-authored-by: Jesse Duffield Signed-off-by: Glenn Vriesman --- edit.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gui.go | 8 +++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/edit.go b/edit.go index b99f74f9..62ac19fb 100644 --- a/edit.go +++ b/edit.go @@ -53,6 +53,20 @@ 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.EditNewLine() + case key == KeySpace: + v.EditWrite(' ') + case key == KeyInsert: + v.Overwrite = !v.Overwrite + case key == KeyCtrlU: + v.EditDeleteToStartOfLine() + case key == KeyCtrlA: + v.EditGotoToStartOfLine() + case key == KeyCtrlE: + v.EditGotoToEndOfLine() + default: + v.EditWrite(ch) } } @@ -63,6 +77,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 end 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..1d1773f4 100644 --- a/gui.go +++ b/gui.go @@ -722,23 +722,29 @@ 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) { + + if kb.viewName == "" && ((v != nil && !v.Editable) || (kb.ch == 0 && kb.key != KeyCtrlU && kb.key != KeyCtrlA && kb.key != KeyCtrlE)) { globalKb = kb } } + if globalKb != nil { return g.execKeybinding(v, globalKb) } + return false, nil } From 57f012571e9d969c62eed2f82b50f1d99b44040c Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Mon, 9 Sep 2019 19:44:19 +0200 Subject: [PATCH 4/6] Gui: Added SetViewBeneath() * This might be useful for some projects See https://github.com/jesseduffield/gocui/commit/bac774b26e2192aca1d8b70506c346ad867553a2 Co-authored-by: Jesse Duffield Signed-off-by: Glenn Vriesman --- gui.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gui.go b/gui.go index 1d1773f4..946028de 100644 --- a/gui.go +++ b/gui.go @@ -183,6 +183,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 { From ce41128562bd72b11a068d4ff3765cb95d7589df Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Fri, 13 Sep 2019 00:53:42 +0200 Subject: [PATCH 5/6] Keybinding: Added keybind parsing * Added Parse() to parse a string to a keybind * Added ParseAll() to parse multiple strings to keybinds * Added MustParse() to parse a string at all costs * Added MustParseAll() to parse multiple strings at all costs * Added an example Co-authored-by: Alex Goodman Signed-off-by: Glenn Vriesman --- _examples/keybinds.go | 69 ++++++++++++++++++ keybinding.go | 158 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 _examples/keybinds.go diff --git a/_examples/keybinds.go b/_examples/keybinds.go new file mode 100644 index 00000000..13bf7256 --- /dev/null +++ b/_examples/keybinds.go @@ -0,0 +1,69 @@ +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) + } + + // 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) + } + + // Now just start a mainloop for the demo + if err = g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} diff --git a/keybinding.go b/keybinding.go index fb8aff3d..8d396f4e 100644 --- a/keybinding.go +++ b/keybinding.go @@ -4,7 +4,19 @@ package gocui -import "github.com/awesome-gocui/termbox-go" +import ( + "errors" + "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 +27,68 @@ type keybinding struct { handler func(*Gui, *View) error } +var ( + ErrorNotExist = errors.New("no such keybind") +) + +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, ErrorNotExist + } + + return key, modifier, nil +} + +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 +} + +func MustParse(input string) (interface{}, Modifier) { + k, m, err := Parse(input) + if err != nil { + panic(err) + } + return k, m +} + +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 +115,82 @@ func (kb *keybinding) matchView(v *View) bool { return kb.viewName == v.name } -// Key represents special keys or keys combinations. -type Key termbox.Key +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 +275,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) From a7ae619b03f562f45cfc6ca68eab4b82cc94e92b Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Sat, 12 Oct 2019 02:02:49 +0200 Subject: [PATCH 6/6] Keybinding: Added blacklist / whitelist support * Allows developers to add / remove keybindings from the blacklist * Added more precise errors * Made the simpleEditor simple again * Added some missing comments Signed-off-by: Glenn Vriesman --- _examples/keybinds.go | 22 +++++++++++++++- edit.go | 10 ++------ gui.go | 60 ++++++++++++++++++++++++++++++++++++++++--- keybinding.go | 15 ++++++----- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/_examples/keybinds.go b/_examples/keybinds.go index 13bf7256..ccd900d0 100644 --- a/_examples/keybinds.go +++ b/_examples/keybinds.go @@ -53,15 +53,35 @@ func main() { 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") + 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 62ac19fb..b5630df3 100644 --- a/edit.go +++ b/edit.go @@ -54,17 +54,11 @@ func simpleEditor(v *View, key Key, ch rune, mod Modifier) { case key == KeyArrowRight: v.MoveCursor(1, 0, false) case key == KeyTab: - v.EditNewLine() + v.EditWrite('\t') case key == KeySpace: v.EditWrite(' ') case key == KeyInsert: v.Overwrite = !v.Overwrite - case key == KeyCtrlU: - v.EditDeleteToStartOfLine() - case key == KeyCtrlA: - v.EditGotoToStartOfLine() - case key == KeyCtrlE: - v.EditGotoToEndOfLine() default: v.EditWrite(ch) } @@ -77,7 +71,7 @@ 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 end of the line. Or if you are already at the start of the line, it deletes the newline character +// 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 { diff --git a/gui.go b/gui.go index 946028de..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. @@ -296,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 @@ -328,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) { @@ -747,7 +787,7 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err return g.execKeybinding(v, kb) } - if kb.viewName == "" && ((v != nil && !v.Editable) || (kb.ch == 0 && kb.key != KeyCtrlU && kb.key != KeyCtrlA && kb.key != KeyCtrlE)) { + if kb.viewName == "" && ((v != nil && !v.Editable) || kb.ch == 0) { globalKb = kb } } @@ -761,12 +801,26 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err // 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 8d396f4e..d294e70d 100644 --- a/keybinding.go +++ b/keybinding.go @@ -5,7 +5,6 @@ package gocui import ( - "errors" "strings" "github.com/awesome-gocui/termbox-go" @@ -27,10 +26,8 @@ type keybinding struct { handler func(*Gui, *View) error } -var ( - ErrorNotExist = errors.New("no such keybind") -) - +// 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])) @@ -55,12 +52,13 @@ func Parse(input string) (interface{}, Modifier, error) { key, exist := translate[strings.Join(cleaned, "")] if !exist { - return nil, ModNone, ErrorNotExist + 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 { @@ -73,6 +71,8 @@ func ParseAll(input []string) (map[interface{}]Modifier, error) { 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 { @@ -81,6 +81,8 @@ func MustParse(input string) (interface{}, Modifier) { 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 { @@ -115,6 +117,7 @@ func (kb *keybinding) matchView(v *View) bool { return kb.viewName == v.name } +// translations for strings to keys var translate = map[string]Key{ "F1": KeyF1, "F2": KeyF2,