Keybinding system for global keys (#135)

Co-authored-by: makeworld <25111343+makeworld-the-better-one@users.noreply.github.com>
This commit is contained in:
Jeff 2020-12-24 13:13:38 -08:00 committed by GitHub
parent 6e3e8a0584
commit f10337e429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 473 additions and 143 deletions

View File

@ -194,7 +194,46 @@ func Init() error {
viper.SetDefault("a-general.page_max_size", 2097152)
viper.SetDefault("a-general.page_max_time", 10)
viper.SetDefault("a-general.emoji_favicons", false)
viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
viper.SetDefault("keybindings.bind_reload", []string{"R", "Ctrl-R"})
viper.SetDefault("keybindings.bind_home", "Backspace")
viper.SetDefault("keybindings.bind_bookmarks", "Ctrl-B")
viper.SetDefault("keybindings.bind_add_bookmark", "Ctrl-D")
viper.SetDefault("keybindings.bind_sub", "Ctrl-A")
viper.SetDefault("keybindings.bind_add_sub", "Ctrl-X")
viper.SetDefault("keybindings.bind_save", "Ctrl-S")
viper.SetDefault("keybindings.bind_pgup", []string{"PgUp", "u"})
viper.SetDefault("keybindings.bind_pgdn", []string{"PgDn", "d"})
viper.SetDefault("keybindings.bind_bottom", "Space")
viper.SetDefault("keybindings.bind_edit", "e")
viper.SetDefault("keybindings.bind_back", []string{"b", "Alt-Left"})
viper.SetDefault("keybindings.bind_forward", []string{"f", "Alt-Right"})
viper.SetDefault("keybindings.bind_new_tab", "Ctrl-T")
viper.SetDefault("keybindings.bind_close_tab", "Ctrl-W")
viper.SetDefault("keybindings.bind_next_tab", "F2")
viper.SetDefault("keybindings.bind_prev_tab", "F1")
viper.SetDefault("keybindings.bind_quit", []string{"Ctrl-C", "Ctrl-Q", "q"})
viper.SetDefault("keybindings.bind_help", "?")
viper.SetDefault("keybindings.bind_link1", "1")
viper.SetDefault("keybindings.bind_link2", "2")
viper.SetDefault("keybindings.bind_link3", "3")
viper.SetDefault("keybindings.bind_link4", "4")
viper.SetDefault("keybindings.bind_link5", "5")
viper.SetDefault("keybindings.bind_link6", "6")
viper.SetDefault("keybindings.bind_link7", "7")
viper.SetDefault("keybindings.bind_link8", "8")
viper.SetDefault("keybindings.bind_link9", "9")
viper.SetDefault("keybindings.bind_link0", "0")
viper.SetDefault("keybindings.bind_tab1", "!")
viper.SetDefault("keybindings.bind_tab2", "@")
viper.SetDefault("keybindings.bind_tab3", "#")
viper.SetDefault("keybindings.bind_tab4", "$")
viper.SetDefault("keybindings.bind_tab5", "%")
viper.SetDefault("keybindings.bind_tab6", "^")
viper.SetDefault("keybindings.bind_tab7", "&")
viper.SetDefault("keybindings.bind_tab8", "*")
viper.SetDefault("keybindings.bind_tab9", "(")
viper.SetDefault("keybindings.bind_tab0", ")")
viper.SetDefault("keybindings.shift_numbers", "")
viper.SetDefault("url-handlers.other", "off")
viper.SetDefault("cache.max_size", 0)
viper.SetDefault("cache.max_pages", 20)
@ -211,6 +250,9 @@ func Init() error {
return err
}
// Setup the key bindings
KeyInit()
// *** Downloads paths, setup, and creation ***
// Setup downloads dir

View File

@ -90,12 +90,51 @@ emoji_favicons = false
[keybindings]
# In the future there will be more settings here.
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
# setup the shift-number bindings: Eg, for US keyboards (the default):
# bind_tab1 = "!"
# bind_tab2 = "@"
# bind_tab3 = "#"
# bind_tab4 = "$"
# bind_tab5 = "%"
# bind_tab6 = "^"
# bind_tab7 = "&"
# bind_tab8 = "*"
# bind_tab9 = "("
# bind_tab0 = ")"
# Hold down shift and press the numbers on your keyboard (1,2,3,4,5,6,7,8,9,0) to set this up.
# It is default set to be accurate for US keyboards.
shift_numbers = "!@#$%^&*()"
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
# Multiple keys can be bound to one command, just use a TOML array.
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
# Ctrl- won't work on all keys, see this for a list:
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
# An example of a TOML array for multiple keys being bound to one command is the default
# binding for reload:
# bind_reload = ["R","Ctrl-R"]
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
# an ANSI terminal)
# The default binding for opening the bottom bar for entering a URL or link number is:
# bind_bottom = "Space"
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
# And, finally, an example of a simple, unmodified character is:
# bind_edit = "e"
# This binds the "e" key to the command to edit the current URL.
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
# typically these are bound to the number keys:
# bind_link1 = "1"
# bind_link2 = "2"
# bind_link3 = "3"
# bind_link4 = "4"
# bind_link5 = "5"
# bind_link6 = "6"
# bind_link7 = "7"
# bind_link8 = "8"
# bind_link9 = "9"
# bind_link0 = "0"
[url-handlers]
# Allows setting the commands to run for various URL schemes.

View File

@ -1,6 +1,10 @@
#!/usr/bin/env bash
head -n 3 default.go | tee default.go > /dev/null
cat > default.go <<-EOF
package config
//go:generate ./default.sh
EOF
echo -n 'var defaultConf = []byte(`' >> default.go
cat ../default-config.toml >> default.go
echo '`)' >> default.go

View File

@ -1,24 +1,237 @@
package config
import (
"errors"
"strings"
"github.com/gdamore/tcell"
"github.com/spf13/viper"
)
// KeyToNum returns the number on the user's keyboard they pressed,
// using the rune returned when when they press Shift+Num.
// The error is not nil if the provided key is invalid.
func KeyToNum(key rune) (int, error) {
runes := []rune(viper.GetString("keybindings.shift_numbers"))
for i := range runes {
if key == runes[i] {
if i == len(runes)-1 {
// Last key is 0, not 10
return 0, nil
}
return i + 1, nil
}
}
return -1, errors.New("provided key is invalid") //nolint:goerr113
// NOTE: CmdLink[1-90] and CmdTab[1-90] need to be in-order and consecutive
// This property is used to simplify key handling in display/display.go
type Command int
const (
CmdInvalid Command = 0
CmdLink1 = 1
CmdLink2 = 2
CmdLink3 = 3
CmdLink4 = 4
CmdLink5 = 5
CmdLink6 = 6
CmdLink7 = 7
CmdLink8 = 8
CmdLink9 = 9
CmdLink0 = 10
CmdTab1 = 11
CmdTab2 = 12
CmdTab3 = 13
CmdTab4 = 14
CmdTab5 = 15
CmdTab6 = 16
CmdTab7 = 17
CmdTab8 = 18
CmdTab9 = 19
CmdTab0 = 20
CmdBottom = iota
CmdEdit
CmdHome
CmdBookmarks
CmdAddBookmark
CmdSave
CmdReload
CmdBack
CmdForward
CmdPgup
CmdPgdn
CmdNewTab
CmdCloseTab
CmdNextTab
CmdPrevTab
CmdQuit
CmdHelp
CmdSub
CmdAddSub
)
type keyBinding struct {
key tcell.Key
mod tcell.ModMask
r rune
}
// Map of active keybindings to commands.
var bindings map[keyBinding]Command
// inversion of tcell.KeyNames, used to simplify config parsing.
// used by parseBinding() below.
var tcellKeys map[string]tcell.Key
// helper function that takes a single keyBinding object and returns
// a string in the format used by the configuration file. Support
// function for GetKeyBinding(), used to make the help panel helpful.
func keyBindingToString(kb keyBinding) (string, bool) {
var prefix string = ""
if kb.mod&tcell.ModAlt == tcell.ModAlt {
prefix = "Alt-"
}
if kb.key == tcell.KeyRune {
if kb.r == ' ' {
return prefix + "Space", true
}
return prefix + string(kb.r), true
}
s, ok := tcell.KeyNames[kb.key]
if ok {
return prefix + s, true
}
return "", false
}
// Get all keybindings for a Command as a string.
// Used by the help panel so bindable keys display with their
// bound values rather than hardcoded defaults.
func GetKeyBinding(cmd Command) string {
var s string = ""
for kb, c := range bindings {
if c == cmd {
t, ok := keyBindingToString(kb)
if ok {
s += t + ", "
}
}
}
if len(s) > 0 {
return s[:len(s)-2]
}
return s
}
// Parse a single keybinding string and add it to the binding map
func parseBinding(cmd Command, binding string) {
var k tcell.Key
var m tcell.ModMask = 0
var r rune = 0
if strings.HasPrefix(binding, "Alt-") {
m = tcell.ModAlt
binding = binding[4:]
}
if len(binding) == 1 {
k = tcell.KeyRune
r = []rune(binding)[0]
} else if len(binding) == 0 {
return
} else if binding == "Space" {
k = tcell.KeyRune
r = ' '
} else {
var ok bool
k, ok = tcellKeys[binding]
if !ok { // Bad keybinding! Quietly ignore...
return
}
if strings.HasPrefix(binding, "Ctrl") {
m += tcell.ModCtrl
}
}
bindings[keyBinding{k, m, r}] = cmd
}
// Generate the bindings map from the TOML configuration file.
// Called by config.Init()
func KeyInit() {
configBindings := map[Command]string{
CmdLink1: "keybindings.bind_link1",
CmdLink2: "keybindings.bind_link2",
CmdLink3: "keybindings.bind_link3",
CmdLink4: "keybindings.bind_link4",
CmdLink5: "keybindings.bind_link5",
CmdLink6: "keybindings.bind_link6",
CmdLink7: "keybindings.bind_link7",
CmdLink8: "keybindings.bind_link8",
CmdLink9: "keybindings.bind_link9",
CmdLink0: "keybindings.bind_link0",
CmdBottom: "keybindings.bind_bottom",
CmdEdit: "keybindings.bind_edit",
CmdHome: "keybindings.bind_home",
CmdBookmarks: "keybindings.bind_bookmarks",
CmdAddBookmark: "keybindings.bind_add_bookmark",
CmdSave: "keybindings.bind_save",
CmdReload: "keybindings.bind_reload",
CmdBack: "keybindings.bind_back",
CmdForward: "keybindings.bind_forward",
CmdPgup: "keybindings.bind_pgup",
CmdPgdn: "keybindings.bind_pgdn",
CmdNewTab: "keybindings.bind_new_tab",
CmdCloseTab: "keybindings.bind_close_tab",
CmdNextTab: "keybindings.bind_next_tab",
CmdPrevTab: "keybindings.bind_prev_tab",
CmdQuit: "keybindings.bind_quit",
CmdHelp: "keybindings.bind_help",
CmdSub: "keybindings.bind_sub",
CmdAddSub: "keybindings.bind_add_sub",
}
// This is split off to allow shift_numbers to override bind_tab[1-90]
// (This is needed for older configs so that the default bind_tab values
// aren't used)
configTabNBindings := map[Command]string{
CmdTab1: "keybindings.bind_tab1",
CmdTab2: "keybindings.bind_tab2",
CmdTab3: "keybindings.bind_tab3",
CmdTab4: "keybindings.bind_tab4",
CmdTab5: "keybindings.bind_tab5",
CmdTab6: "keybindings.bind_tab6",
CmdTab7: "keybindings.bind_tab7",
CmdTab8: "keybindings.bind_tab8",
CmdTab9: "keybindings.bind_tab9",
CmdTab0: "keybindings.bind_tab0",
}
tcellKeys = make(map[string]tcell.Key)
bindings = make(map[keyBinding]Command)
for k, kname := range tcell.KeyNames {
tcellKeys[kname] = k
}
for c, allb := range configBindings {
for _, b := range viper.GetStringSlice(allb) {
parseBinding(c, b)
}
}
// Backwards compatibility with the old shift_numbers config line.
shiftNumbers := []rune(viper.GetString("keybindings.shift_numbers"))
if len(shiftNumbers) > 0 && len(shiftNumbers) <= 10 {
for i, r := range shiftNumbers {
bindings[keyBinding{tcell.KeyRune, 0, r}] = CmdTab1 + Command(i)
}
} else {
for c, allb := range configTabNBindings {
for _, b := range viper.GetStringSlice(allb) {
parseBinding(c, b)
}
}
}
}
// Used by the display package to turn a tcell.EventKey into a Command
func TranslateKeyEvent(e *tcell.EventKey) Command {
var ok bool
var cmd Command
k := e.Key()
if k == tcell.KeyRune {
cmd, ok = bindings[keyBinding{k, e.Modifiers(), e.Rune()}]
} else { // Sometimes tcell sets e.Rune() on non-KeyRune events.
cmd, ok = bindings[keyBinding{k, e.Modifiers(), 0}]
}
if ok {
return cmd
}
return CmdInvalid
}

View File

@ -87,12 +87,51 @@ emoji_favicons = false
[keybindings]
# In the future there will be more settings here.
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
# setup the shift-number bindings: Eg, for US keyboards (the default):
# bind_tab1 = "!"
# bind_tab2 = "@"
# bind_tab3 = "#"
# bind_tab4 = "$"
# bind_tab5 = "%"
# bind_tab6 = "^"
# bind_tab7 = "&"
# bind_tab8 = "*"
# bind_tab9 = "("
# bind_tab0 = ")"
# Hold down shift and press the numbers on your keyboard (1,2,3,4,5,6,7,8,9,0) to set this up.
# It is default set to be accurate for US keyboards.
shift_numbers = "!@#$%^&*()"
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
# Multiple keys can be bound to one command, just use a TOML array.
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
# Ctrl- won't work on all keys, see this for a list:
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
# An example of a TOML array for multiple keys being bound to one command is the default
# binding for reload:
# bind_reload = ["R","Ctrl-R"]
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
# an ANSI terminal)
# The default binding for opening the bottom bar for entering a URL or link number is:
# bind_bottom = "Space"
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
# And, finally, an example of a simple, unmodified character is:
# bind_edit = "e"
# This binds the "e" key to the command to edit the current URL.
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
# typically these are bound to the number keys:
# bind_link1 = "1"
# bind_link2 = "2"
# bind_link3 = "3"
# bind_link4 = "4"
# bind_link5 = "5"
# bind_link6 = "6"
# bind_link7 = "7"
# bind_link8 = "8"
# bind_link9 = "9"
# bind_link0 = "0"
[url-handlers]
# Allows setting the commands to run for various URL schemes.

View File

@ -272,43 +272,36 @@ func Init(version, commit, builtBy string) {
return event
}
// To add a configurable global key command, you'll need to update one of
// the two switch statements here. You'll also need to add an enum entry in
// config/keybindings.go, update KeyInit() in config/keybindings.go, add a default
// keybinding in config/config.go and update the help panel in display/help.go
cmd := config.TranslateKeyEvent(event)
if tabs[curTab].mode == tabModeDone {
// All the keys and operations that can only work while NOT loading
// History arrow keys
if event.Modifiers() == tcell.ModAlt {
if event.Key() == tcell.KeyLeft {
histBack(tabs[curTab])
return nil
}
if event.Key() == tcell.KeyRight {
histForward(tabs[curTab])
return nil
}
}
//nolint:exhaustive
switch event.Key() {
case tcell.KeyCtrlR:
switch cmd {
case config.CmdReload:
Reload()
return nil
case tcell.KeyCtrlH:
case config.CmdHome:
URL(viper.GetString("a-general.home"))
return nil
case tcell.KeyCtrlB:
case config.CmdBookmarks:
Bookmarks(tabs[curTab])
tabs[curTab].addToHistory("about:bookmarks")
return nil
case tcell.KeyCtrlD:
case config.CmdAddBookmark:
go addBookmark()
return nil
case tcell.KeyPgUp:
case config.CmdPgup:
tabs[curTab].pageUp()
return nil
case tcell.KeyPgDn:
case config.CmdPgdn:
tabs[curTab].pageDown()
return nil
case tcell.KeyCtrlS:
case config.CmdSave:
if tabs[curTab].hasContent() {
savePath, err := downloadPage(tabs[curTab].page)
if err != nil {
@ -320,66 +313,48 @@ func Init(version, commit, builtBy string) {
Info("The current page has no content, so it couldn't be downloaded.")
}
return nil
case tcell.KeyCtrlA:
Subscriptions(tabs[curTab], "about:subscriptions")
tabs[curTab].addToHistory("about:subscriptions")
return nil
case tcell.KeyCtrlX:
go addSubscription()
return nil
case tcell.KeyRune:
// Regular key was sent
switch string(event.Rune()) {
case " ":
case config.CmdBottom:
// Space starts typing, like Bombadillo
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
bottomBar.SetText("")
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
App.SetFocus(bottomBar)
return nil
case "e":
case config.CmdEdit:
// Letter e allows to edit current URL
bottomBar.SetLabel("[::b]Edit URL: [::-]")
bottomBar.SetText(tabs[curTab].page.URL)
App.SetFocus(bottomBar)
return nil
case "R":
Reload()
return nil
case "b":
case config.CmdBack:
histBack(tabs[curTab])
return nil
case "f":
case config.CmdForward:
histForward(tabs[curTab])
return nil
case "u":
tabs[curTab].pageUp()
case config.CmdSub:
Subscriptions(tabs[curTab], "about:subscriptions")
tabs[curTab].addToHistory("about:subscriptions")
return nil
case "d":
tabs[curTab].pageDown()
case config.CmdAddSub:
go addSubscription()
return nil
}
// Number key: 1-9, 0
i, err := strconv.Atoi(string(event.Rune()))
if err == nil {
if i == 0 {
i = 10 // 0 key is for link 10
}
if i <= len(tabs[curTab].page.Links) && i > 0 {
// Number key: 1-9, 0, LINK1-LINK10
if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
if int(cmd) <= len(tabs[curTab].page.Links) {
// It's a valid link number
followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[i-1])
followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[cmd-1])
return nil
}
}
}
}
// All the keys and operations that can work while a tab IS loading
//nolint:exhaustive
switch event.Key() {
case tcell.KeyCtrlT:
switch cmd {
case config.CmdNewTab:
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Selected)
if err != nil {
@ -392,45 +367,33 @@ func Init(version, commit, builtBy string) {
NewTab()
}
return nil
case tcell.KeyCtrlW:
case config.CmdCloseTab:
CloseTab()
return nil
case tcell.KeyCtrlQ:
case config.CmdQuit:
Stop()
return nil
case tcell.KeyCtrlC:
Stop()
return nil
case tcell.KeyF1:
case config.CmdPrevTab:
// Wrap around, allow for modulo with negative numbers
n := NumTabs()
SwitchTab((((curTab - 1) % n) + n) % n)
return nil
case tcell.KeyF2:
case config.CmdNextTab:
SwitchTab((curTab + 1) % NumTabs())
return nil
case tcell.KeyRune:
// Regular key was sent
if num, err := config.KeyToNum(event.Rune()); err == nil {
// It's a Shift+Num key
if num == 0 {
// Zero key goes to the last tab
SwitchTab(NumTabs() - 1)
} else {
SwitchTab(num - 1)
}
return nil
}
switch string(event.Rune()) {
case "q":
Stop()
return nil
case "?":
case config.CmdHelp:
Help()
return nil
}
if cmd >= config.CmdTab1 && cmd <= config.CmdTab0 {
if cmd == config.CmdTab0 {
// Zero key goes to the last tab
SwitchTab(NumTabs() - 1)
} else {
SwitchTab(int(cmd - config.CmdTab1))
}
return nil
}
// Let another element handle the event, it's not a special global key

View File

@ -1,10 +1,12 @@
package display
import (
"fmt"
"strconv"
"strings"
"github.com/gdamore/tcell"
"github.com/makeworld-the-better-one/amfora/config"
"gitlab.com/tslocum/cview"
)
@ -12,41 +14,39 @@ var helpCells = strings.TrimSpace(`
?|Bring up this help. You can scroll!
Esc|Leave the help
Arrow keys, h/j/k/l|Scroll and move a page.
PgUp, u|Go up a page in document
PgDn, d|Go down a page in document
%s|Go up a page in document
%s|Go down a page in document
g|Go to top of document
G|Go to bottom of document
Tab|Navigate to the next item in a popup.
Shift-Tab|Navigate to the previous item in a popup.
b, Alt-Left|Go back in the history
f, Alt-Right|Go forward in the history
spacebar|Open bar at the bottom - type a URL, link number, search term.
%s|Go back in the history
%s|Go forward in the history
%s|Open bar at the bottom - type a URL, link number, search term.
|You can also type two dots (..) to go up a directory in the URL.
|Typing new:N will open link number N in a new tab
|instead of the current one.
Numbers|Go to links 1-10 respectively.
e|Edit current URL
%s|Go to links 1-10 respectively.
%s|Edit current URL
Enter, Tab|On a page this will start link highlighting.
|Press Tab and Shift-Tab to pick different links.
|Press Enter again to go to one, or Esc to stop.
Shift-NUMBER|Go to a specific tab.
Shift-0, )|Go to the last tab.
F1|Previous tab
F2|Next tab
Ctrl-H|Go home
Ctrl-T|New tab, or if a link is selected,
%s|Go to a specific tab. (Default: Shift-NUMBER)
%s|Go to the last tab.
%s|Previous tab
%s|Next tab
%s|Go home
%s|New tab, or if a link is selected,
|this will open the link in a new tab.
Ctrl-W|Close tab. For now, only the right-most tab can be closed.
Ctrl-R, R|Reload a page, discarding the cached version.
%s|Close tab. For now, only the right-most tab can be closed.
%s|Reload a page, discarding the cached version.
|This can also be used if you resize your terminal.
Ctrl-B|View bookmarks
Ctrl-D|Add, change, or remove a bookmark for the current page.
Ctrl-S|Save the current page to your downloads.
Ctrl-A|View subscriptions
Ctrl-X|Add or update a subscription
q, Ctrl-Q|Quit
Ctrl-C|Hard quit. This can be used when in the middle of downloading,
|for example.
%s|View bookmarks
%s|Add, change, or remove a bookmark for the current page.
%s|Save the current page to your downloads.
%s|View subscriptions
%s|Add or update a subscription
%s|Quit
`)
var helpTable = cview.NewTable().
@ -71,6 +71,36 @@ func helpInit() {
App.Draw()
}
})
tabKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdTab1), ",")[0],
strings.Split(config.GetKeyBinding(config.CmdTab9), ",")[0])
linkKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdLink1), ",")[0],
strings.Split(config.GetKeyBinding(config.CmdLink0), ",")[0])
helpCells = fmt.Sprintf(helpCells,
config.GetKeyBinding(config.CmdPgup),
config.GetKeyBinding(config.CmdPgdn),
config.GetKeyBinding(config.CmdBack),
config.GetKeyBinding(config.CmdForward),
config.GetKeyBinding(config.CmdBottom),
linkKeys,
config.GetKeyBinding(config.CmdEdit),
tabKeys,
config.GetKeyBinding(config.CmdTab0),
config.GetKeyBinding(config.CmdPrevTab),
config.GetKeyBinding(config.CmdNextTab),
config.GetKeyBinding(config.CmdHome),
config.GetKeyBinding(config.CmdNewTab),
config.GetKeyBinding(config.CmdCloseTab),
config.GetKeyBinding(config.CmdReload),
config.GetKeyBinding(config.CmdBookmarks),
config.GetKeyBinding(config.CmdAddBookmark),
config.GetKeyBinding(config.CmdSave),
config.GetKeyBinding(config.CmdSub),
config.GetKeyBinding(config.CmdAddSub),
config.GetKeyBinding(config.CmdQuit),
)
rows := strings.Count(helpCells, "\n") + 1
cells := strings.Split(
strings.ReplaceAll(helpCells, "\n", "|"),