Skip to content

Commit

Permalink
Keybinding system for global keys (#135)
Browse files Browse the repository at this point in the history
Co-authored-by: makeworld <25111343+makeworld-the-better-one@users.noreply.github.com>
  • Loading branch information
phaedrus-jaf and makew0rld authored Dec 24, 2020
1 parent 6e3e8a0 commit f10337e
Show file tree
Hide file tree
Showing 7 changed files with 477 additions and 147 deletions.
44 changes: 43 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -211,6 +250,9 @@ func Init() error {
return err
}

// Setup the key bindings
KeyInit()

// *** Downloads paths, setup, and creation ***

// Setup downloads dir
Expand Down
51 changes: 45 additions & 6 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,51 @@ emoji_favicons = false
[keybindings]
# In the future there will be more settings here.
# 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 = "!@#$%^&*()"
# 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 = ")"
# 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.
Expand Down
8 changes: 6 additions & 2 deletions config/default.sh
Original file line number Diff line number Diff line change
@@ -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
echo '`)' >> default.go
239 changes: 226 additions & 13 deletions config/keybindings.go
Original file line number Diff line number Diff line change
@@ -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
// 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)
}
return i + 1, nil
}
}
return -1, errors.New("provided key is invalid") //nolint:goerr113
}

// 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
}
Loading

0 comments on commit f10337e

Please sign in to comment.